am 082ea423: am 49826d43: Reconcile with ics-mr1-release

* commit '082ea4230f5892379cb0eebfd87d1594e51987ec':
  Patch 2 for MR1.
  Patch for MR1.
diff --git a/Android.mk b/Android.mk
index 0f5170f..6584c4a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -8,17 +8,29 @@
 
 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
+
+LOCAL_REQUIRED_MODULES := libjni_mosaic
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 
 include $(BUILD_PACKAGE)
 
-# Use the following include to make our test apk.
+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..c907da1 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,9 +35,12 @@
 
     <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: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:theme="@style/Theme.MovieActivity"
                 android:configChanges="orientation|keyboardHidden|screenSize">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
@@ -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" />
@@ -238,8 +249,58 @@
                 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:screenOrientation="behind"
+                android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.media.action.IMAGE_CAPTURE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.media.action.STILL_IMAGE_CAMERA" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <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:screenOrientation="behind"
+                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:screenOrientation="behind"
+                android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
+        </activity>
         <receiver android:name="com.android.gallery3d.gadget.PhotoAppWidgetProvider"
                 android:label="@string/appwidget_title">
             <intent-filter>
@@ -256,6 +317,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/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/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/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/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/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/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-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/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/main.xml b/res/layout/main.xml
index b71ea50..7dfe57a 100644
--- a/res/layout/main.xml
+++ b/res/layout/main.xml
@@ -1,5 +1,6 @@
 <?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">
diff --git a/res/layout/movie_view.xml b/res/layout/movie_view.xml
index bd0415c..4e645cf 100644
--- a/res/layout/movie_view.xml
+++ b/res/layout/movie_view.xml
@@ -15,7 +15,7 @@
 -->
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/root"
+    android:id="@+id/movie_view_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
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
index 023a93b..21f802a 100644
--- a/res/menu-land/photo.xml
+++ b/res/menu-land/photo.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_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..749b7f9 100644
--- a/res/menu/albumset.xml
+++ b/res/menu/albumset.xml
@@ -30,4 +30,7 @@
     <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: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..685627a 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="never" />
     <item android:id="@+id/action_slideshow"
             android:icon="@drawable/ic_menu_slideshow_holo_light"
             android:title="@string/slideshow"
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 8feab87..af3e90c 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Kanselleer"</string>
     <string name="share" msgid="3619042788254195341">"Deling"</string>
     <string name="select_all" msgid="3403283025220282175">"Kies almal"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Kon nie die lys van albums 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>
@@ -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/strings.xml b/res/values-am/strings.xml
index e93b7d9..ace5497 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"ለመጀመር ፊት ንካ::"</string>
     <string name="saving_image" msgid="7270334453636349407">"ምስል በማስቀመጥ ላይ..."</string>
     <string name="save_error" msgid="6857408774183654970">"የተቀመቀመውን ምስል ማስቀመጥ አልተቻለም::"</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"ይቅር"</string>
     <string name="share" msgid="3619042788254195341">"አጋራ"</string>
     <string name="select_all" msgid="3403283025220282175">"ሁሉንም ምረጥ"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"የአልበሞችን ዝርዝር ለማውረድ አልተቻለም፡፡ በኋላ  ደግመህ ሞክር፡፡"</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>
@@ -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/strings.xml b/res/values-ar/strings.xml
index 922919c..6bdeecc 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"المس وجهًا للبدء."</string>
     <string name="saving_image" msgid="7270334453636349407">"جارٍ حفظ الصورة..."</string>
     <string name="save_error" msgid="6857408774183654970">"تعذر حفظ الصورة التي تم اقتصاصها."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"إلغاء"</string>
     <string name="share" msgid="3619042788254195341">"مشاركة"</string>
     <string name="select_all" msgid="3403283025220282175">"تحديد الكل"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"تعذر تحميل قائمة الألبومات. أعد المحاولة لاحقًا."</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>
@@ -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/strings.xml b/res/values-be/strings.xml
index 718c81f..b2b322c 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"Дакранiцеся да твару, каб пачаць."</string>
     <string name="saving_image" msgid="7270334453636349407">"Захаванне выявы..."</string>
     <string name="save_error" msgid="6857408774183654970">"Немагчыма захаваць абрэзаную выяву."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Адмяніць"</string>
     <string name="share" msgid="3619042788254195341">"Апублікаваць"</string>
     <string name="select_all" msgid="3403283025220282175">"Выбраць усё"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Немагчыма спампаваць спіс альбомаў. Паўтарыце спробу пазней."</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>
@@ -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/strings.xml b/res/values-bg/strings.xml
index b4f7474..e3619f7 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"Докоснете лице за начало."</string>
     <string name="saving_image" msgid="7270334453636349407">"Снимката се запазва..."</string>
     <string name="save_error" msgid="6857408774183654970">"Подрязаното изобр. не можа да се запази."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Отказ"</string>
     <string name="share" msgid="3619042788254195341">"Споделяне"</string>
     <string name="select_all" msgid="3403283025220282175">"Избиране на всички"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Списъкът с албуми не можа да се изтегли. Опитайте отново по-късно."</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>
@@ -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/strings.xml b/res/values-ca/strings.xml
index e053e4e..c385221 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"No s\'ha pogut baixar la llista d\'àlbums. 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>
@@ -162,4 +166,9 @@
     <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/strings.xml b/res/values-cs/strings.xml
index 780295e..046830c 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Seznam alb 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>
@@ -162,4 +166,9 @@
     <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ímek obrazovky"</string>
+    <string name="help" msgid="7368960711153618354">"Nápověda"</string>
 </resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 3130cbf..0c11da4 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -29,11 +29,12 @@
     <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="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>
@@ -45,10 +46,13 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Annuller"</string>
     <string name="share" msgid="3619042788254195341">"Del"</string>
-    <string name="select_all" msgid="3403283025220282175">"Marker alle"</string>
+    <string name="select_all" msgid="3403283025220282175">"Markér alle"</string>
     <string name="deselect_all" msgid="5758897506061723684">"Fjern markering af alle"</string>
     <string name="slideshow" msgid="4355906903247112975">"Diasshow"</string>
     <string name="details" msgid="8415120088556445230">"Detaljer"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Listen over albummer kunne ikke downloades. 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>
@@ -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/strings.xml b/res/values-de/strings.xml
index a979399..05b4170 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Abbrechen"</string>
     <string name="share" msgid="3619042788254195341">"Teilen"</string>
     <string name="select_all" msgid="3403283025220282175">"Alle auswählen"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Die Albenliste konnte 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>
@@ -127,7 +131,7 @@
     <string name="iso" msgid="5028296664327335940">"ISO"</string>
     <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
     <string name="manual" msgid="6608905477477607865">"Manuell"</string>
-    <string name="auto" msgid="4296941368722892821">"Autom."</string>
+    <string name="auto" msgid="4296941368722892821">"Automatisch"</string>
     <string name="flash_on" msgid="7891556231891837284">"Blitz ausgelöst"</string>
     <string name="flash_off" msgid="1445443413822680010">"Ohne Blitz"</string>
   <plurals name="make_albums_available_offline">
@@ -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/strings.xml b/res/values-el/strings.xml
index 4d1e2e0..4b5b3c3 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"Αγγίξτε κάποιο πρόσωπο για να ξεκινήσετε."</string>
     <string name="saving_image" msgid="7270334453636349407">"Αποθήκευση εικόνας..."</string>
     <string name="save_error" msgid="6857408774183654970">"Αδυναμία αποθήκευσης αποκομμένης εικόνας"</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Ακύρωση"</string>
     <string name="share" msgid="3619042788254195341">"Κοινή χρήση"</string>
     <string name="select_all" msgid="3403283025220282175">"Επιλογή όλων"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Δεν ήταν δυνατή η λήψη της λίστας λευκωμάτων. Δοκιμάστε ξανά αργότερα."</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>
@@ -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/strings.xml b/res/values-en-rGB/strings.xml
index ca72583..0bdceb4 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Cancel"</string>
     <string name="share" msgid="3619042788254195341">"Share"</string>
     <string name="select_all" msgid="3403283025220282175">"Select all"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Couldn\'t download the list of albums. 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>
@@ -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">"Was this helpful?"</string>
 </resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index af0aeac..9a13cdf 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
     <string name="share" msgid="3619042788254195341">"Compartir"</string>
     <string name="select_all" msgid="3403283025220282175">"Seleccionar todo"</string>
@@ -53,7 +57,7 @@
     <string name="slideshow" msgid="4355906903247112975">"Presentación de diapositivas"</string>
     <string name="details" msgid="8415120088556445230">"Detalles"</string>
     <string name="details_title" msgid="2611396603977441273">"%1$d de %2$d elementos:"</string>
-    <string name="close" msgid="5585646033158453043">"Cerca"</string>
+    <string name="close" msgid="5585646033158453043">"Cerrar"</string>
     <string name="switch_to_camera" msgid="7280111806675169992">"Cambiar a cámara"</string>
   <plurals name="number_of_items_selected">
     <item quantity="zero" msgid="2142579311530586258">"%1$d seleccionado(s)"</item>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"No se pudo descargar la lista de álbumes. 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>
@@ -155,11 +159,16 @@
     <string name="widget_type" msgid="1364653978966343448">"Elegir imágenes"</string>
     <string name="slideshow_dream_name" msgid="6915963319933437083">"Presentación de diapositivas"</string>
     <string name="albums" msgid="7320787705180057947">"Álbumes"</string>
-    <string name="times" msgid="2023033894889499219">"Horarios"</string>
-    <string name="locations" msgid="6649297994083130305">"Ubicaciones"</string>
+    <string name="times" msgid="2023033894889499219">"Fecha"</string>
+    <string name="locations" msgid="6649297994083130305">"Ubicación"</string>
     <string name="people" msgid="4114003823747292747">"Personas"</string>
     <string name="tags" msgid="5539648765482935955">"Etiquetas"</string>
     <string name="group_by" msgid="4308299657902209357">"Agrupar por"</string>
     <string name="settings" msgid="1534847740615665736">"Configuración"</string>
     <string name="add_account" msgid="4271217504968243974">"Agregar cuenta"</string>
+    <string name="folder_camera" msgid="4714658994809533480">"Cámara"</string>
+    <string name="folder_download" msgid="7186215137642323932">"Descargas"</string>
+    <string name="folder_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/strings.xml b/res/values-es/strings.xml
index b0b2178..dcca6e6 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
     <string name="share" msgid="3619042788254195341">"Compartir"</string>
     <string name="select_all" msgid="3403283025220282175">"Seleccionar todo"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"No se ha podido descargar la lista de álbumes. 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>
@@ -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/strings.xml b/res/values-et/strings.xml
index 1072aa7..a7bc4ed 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Albumite loendit ei saa 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>
@@ -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/strings.xml b/res/values-fa/strings.xml
index 9017b40..cecd272 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"برای شروع یک چهره را لمس کنید."</string>
     <string name="saving_image" msgid="7270334453636349407">"در حال ذخیره عکس..."</string>
     <string name="save_error" msgid="6857408774183654970">"ذخیره تصویر برش‌خورده امکان‌پذیر نیست."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"لغو"</string>
     <string name="share" msgid="3619042788254195341">"اشتراک گذاری"</string>
     <string name="select_all" msgid="3403283025220282175">"انتخاب همه"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"لیست آلبوم‌ها را نمی‌توان دانلود کرد. بعداً دوباره امتحان کنید."</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>
@@ -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/strings.xml b/res/values-fi/strings.xml
index 2527a70..9f370ea 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Peruuta"</string>
     <string name="share" msgid="3619042788254195341">"Jaa"</string>
     <string name="select_all" msgid="3403283025220282175">"Valitse kaikki"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Albumiluetteloa 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>
@@ -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/strings.xml b/res/values-fr/strings.xml
index 4873b9f..36c04f6 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Annuler"</string>
     <string name="share" msgid="3619042788254195341">"Partager"</string>
     <string name="select_all" msgid="3403283025220282175">"Tout sélectionner"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Impossible de télécharger la liste des albums. 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>
@@ -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/strings.xml b/res/values-hi/strings.xml
index 7788858..93d95fa 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"शुरू करने के लिए कोई चेहरा स्‍पर्श करें."</string>
     <string name="saving_image" msgid="7270334453636349407">"चित्र सहेज रहा है…"</string>
     <string name="save_error" msgid="6857408774183654970">"काट-छांट की गई छवि को नहीं सहेज सका."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"रद्द करें"</string>
     <string name="share" msgid="3619042788254195341">"शेयर करें"</string>
     <string name="select_all" msgid="3403283025220282175">"सभी का चयन करें"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"एल्बम की सूची डाउनलोड नहीं की जा सकी. बाद में पुन: प्रयास करें."</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>
@@ -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/strings.xml b/res/values-hr/strings.xml
index 6c57869..f2c755b 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Odustani"</string>
     <string name="share" msgid="3619042788254195341">"Podijeli"</string>
     <string name="select_all" msgid="3403283025220282175">"Odaberi sve"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Preuzimanje popisa 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>
@@ -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/strings.xml b/res/values-hu/strings.xml
index dd905ae..f07ab56 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Nem sikerült letölteni az albumok listáját. 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>
@@ -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/strings.xml b/res/values-in/strings.xml
index 9f62b25..48d03ed 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,12 +46,15 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Tidak dapat mengunduh daftar album. 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>
@@ -102,12 +106,12 @@
     <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/strings.xml b/res/values-it/strings.xml
index 321bf67..e1975ac 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Annulla"</string>
     <string name="share" msgid="3619042788254195341">"Condividi"</string>
     <string name="select_all" msgid="3403283025220282175">"Seleziona tutti"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Download dell\'elenco di 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>
@@ -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/strings.xml b/res/values-iw/strings.xml
index 76e7133..41bd60a 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"גע בפנים כלשהם כדי להתחיל."</string>
     <string name="saving_image" msgid="7270334453636349407">"שומר תמונה..."</string>
     <string name="save_error" msgid="6857408774183654970">"לא ניתן לשמור את התמונה החתוכה."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"ביטול"</string>
     <string name="share" msgid="3619042788254195341">"שיתוף"</string>
     <string name="select_all" msgid="3403283025220282175">"בחר הכול"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"לא ניתן להוריד את רשימת האלבומים. נסה שוב מאוחר יותר."</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>
@@ -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/strings.xml b/res/values-ja/strings.xml
index 460fe6d..6ad2735 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"始めるには顔をタップします。"</string>
     <string name="saving_image" msgid="7270334453636349407">"写真を保存中…"</string>
     <string name="save_error" msgid="6857408774183654970">"トリミングした画像を保存できません。"</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"キャンセル"</string>
     <string name="share" msgid="3619042788254195341">"共有"</string>
     <string name="select_all" msgid="3403283025220282175">"すべて選択"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"アルバムのリストをダウンロードできませんでした。しばらくしてからもう一度お試しください。"</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>
@@ -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/strings.xml b/res/values-ko/strings.xml
index 1f8f45b..e285dce 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"시작하려면 얼굴을 터치하세요."</string>
     <string name="saving_image" msgid="7270334453636349407">"사진 저장 중..."</string>
     <string name="save_error" msgid="6857408774183654970">"잘린 이미지를 저장하지 못했습니다."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"취소"</string>
     <string name="share" msgid="3619042788254195341">"공유"</string>
     <string name="select_all" msgid="3403283025220282175">"모두 선택"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"앨범 목록을 다운로드하지 못했습니다. 나중에 다시 시도해 주세요."</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>
@@ -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-lt/strings.xml b/res/values-lt/strings.xml
index 1a2b9fd..677cc5b 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Atšaukti"</string>
     <string name="share" msgid="3619042788254195341">"Bendrinti"</string>
     <string name="select_all" msgid="3403283025220282175">"Pasirinkti viską"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Albumų sąrašo 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>
@@ -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/strings.xml b/res/values-lv/strings.xml
index 2da2540..c66b0cc 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Atcelt"</string>
     <string name="share" msgid="3619042788254195341">"Dalies"</string>
     <string name="select_all" msgid="3403283025220282175">"Atlasīt visu"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Nevarēja lejupielādēt albumu sarakstu. 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>
@@ -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/strings.xml b/res/values-ms/strings.xml
index eead8b1..0a0c4dc 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Batal"</string>
     <string name="share" msgid="3619042788254195341">"Kongsi"</string>
     <string name="select_all" msgid="3403283025220282175">"Pilih semua"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Tidak dapat memuat turun senarai album. 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>
@@ -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/strings.xml b/res/values-nb/strings.xml
index d8a708a..20ec589 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Avbryt"</string>
     <string name="share" msgid="3619042788254195341">"Del"</string>
     <string name="select_all" msgid="3403283025220282175">"Velg alle"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Kunne ikke laste ned albumlisten. 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>
@@ -162,4 +166,9 @@
     <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/strings.xml b/res/values-nl/strings.xml
index 9e43d85..ec93df3 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Annuleren"</string>
     <string name="share" msgid="3619042788254195341">"Delen"</string>
     <string name="select_all" msgid="3403283025220282175">"Alles selecteren"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Kan de lijst met albums 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>
@@ -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/strings.xml b/res/values-pl/strings.xml
index 82f4310..ead7859 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Anuluj"</string>
     <string name="share" msgid="3619042788254195341">"Udostępnij"</string>
     <string name="select_all" msgid="3403283025220282175">"Zaznacz wszystkie"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Nie można pobrać listy albumów. 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>
@@ -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/strings.xml b/res/values-pt-rPT/strings.xml
index d0b54fe..d96049b 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
     <string name="share" msgid="3619042788254195341">"Partilhar"</string>
     <string name="select_all" msgid="3403283025220282175">"Selecionar tudo"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Não foi possível transferir a lista de álbuns. 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>
@@ -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/strings.xml b/res/values-pt/strings.xml
index a5fce6c..9b516df 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
     <string name="share" msgid="3619042788254195341">"Compartilhar"</string>
     <string name="select_all" msgid="3403283025220282175">"Selecionar tudo"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Não foi possível fazer download da lista de álbuns. 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>
@@ -162,4 +166,9 @@
     <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 cbc2659..6cfa1d0 100644
--- a/res/values-rm/strings.xml
+++ b/res/values-rm/strings.xml
@@ -20,7 +20,8 @@
     <string name="gadget_title" msgid="259405922673466798">"Rom da maletgs"</string>
     <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
     <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
-    <!-- outdated translation 3697303290960009886 -->     <string name="movie_view_label" msgid="3526526872644898229">"Films"</string>
+    <!-- no translation found for movie_view_label (3526526872644898229) -->
+    <skip />
     <string name="loading_video" msgid="4013492720121891585">"Chargiar il video…"</string>
     <!-- no translation found for loading_image (1200894415793838191) -->
     <skip />
@@ -33,11 +34,15 @@
     <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>
-    <!-- outdated translation 8140440041190264400 -->     <string name="crop_save_text" msgid="4972430481677741184">"Memorisar"</string>
-    <!-- outdated translation 3127018992717032779 -->     <string name="multiface_crop_help" msgid="2554690102655855657">"Smatgai sin ina fatscha per cumenzar."</string>
+    <!-- no translation found for crop_save_text (152200178986698300) -->
+    <skip />
+    <!-- no translation found for multiface_crop_help (2554690102655855657) -->
+    <skip />
     <string name="saving_image" msgid="7270334453636349407">"Memorisar il maletg..."</string>
     <!-- no translation found for save_error (6857408774183654970) -->
     <skip />
@@ -53,14 +58,20 @@
     <!-- no translation found for select_group (6744208543323307114) -->
     <skip />
     <string name="set_image" msgid="2331476809308010401">"Definir il maletg sco"</string>
-    <!-- outdated translation 9222901738515471972 -->     <string name="wallpaper" msgid="140165383777262070">"La culissa vegn configurada. Spetgai per plaschair..."</string>
+    <!-- no translation found for wallpaper (140165383777262070) -->
+    <skip />
     <string name="camera_setas_wallpaper" msgid="797463183863414289">"Culissa"</string>
     <string name="delete" msgid="2839695998251824487">"Stizzar"</string>
-    <!-- outdated translation 5731757674837098707 -->     <string name="confirm_delete" msgid="7358770022173660511">"Confermar il stizzar"</string>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Interrumper"</string>
     <string name="share" msgid="3619042788254195341">"Cundivider"</string>
-    <!-- outdated translation 8623593677101437957 -->     <string name="select_all" msgid="3403283025220282175">"Selecziunar tut"</string>
-    <!-- outdated translation 7397531298370285581 -->     <string name="deselect_all" msgid="5758897506061723684">"Deselecziunar tut"</string>
+    <!-- no translation found for select_all (3403283025220282175) -->
+    <skip />
+    <!-- no translation found for deselect_all (5758897506061723684) -->
+    <skip />
     <string name="slideshow" msgid="4355906903247112975">"Preschentaziun da dia"</string>
     <string name="details" msgid="8415120088556445230">"Detagls"</string>
     <!-- no translation found for details_title (2611396603977441273) -->
@@ -94,7 +105,8 @@
     <!-- no translation found for crop_action (3427470284074377001) -->
     <skip />
     <string name="set_as" msgid="3636764710790507868">"Definir sco"</string>
-    <!-- outdated translation 7917736494827857757 -->     <string name="video_err" msgid="7003051631792271009">"Impussibel da reproducir il video"</string>
+    <!-- no translation found for video_err (7003051631792271009) -->
+    <skip />
     <!-- no translation found for group_by_location (316641628989023253) -->
     <skip />
     <!-- no translation found for group_by_time (9046168567717963573) -->
@@ -113,15 +125,15 @@
     <skip />
     <!-- no translation found for no_connectivity (7164037617297293668) -->
     <skip />
-    <!-- no translation found for sync_album_error (2218733298953719785) -->
+    <!-- no translation found for sync_album_error (1020688062900977530) -->
     <skip />
-    <!-- no translation found for sync_album_set_error (9016732535181154028) -->
+    <!-- no translation found for sync_album_set_error (3250258387046904444) -->
     <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 />
@@ -248,4 +260,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/strings.xml b/res/values-ro/strings.xml
index 3caf4bc..0b531fb 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Descărcarea listei de albume 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>
@@ -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/strings.xml b/res/values-ru/strings.xml
index 1212a91..1d62e92 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"Нажмите на лицо, чтобы начать."</string>
     <string name="saving_image" msgid="7270334453636349407">"Сохранение картинки..."</string>
     <string name="save_error" msgid="6857408774183654970">"Изображение не сохранено."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Отмена"</string>
     <string name="share" msgid="3619042788254195341">"Отправить"</string>
     <string name="select_all" msgid="3403283025220282175">"Выбрать все"</string>
@@ -54,21 +58,21 @@
     <string name="details" msgid="8415120088556445230">"Сведения"</string>
     <string name="details_title" msgid="2611396603977441273">"Элементов %1$d из %2$d:"</string>
     <string name="close" msgid="5585646033158453043">"Закрыть"</string>
-    <string name="switch_to_camera" msgid="7280111806675169992">"Переключиться на камеру"</string>
+    <string name="switch_to_camera" msgid="7280111806675169992">"Режим \"Фото\""</string>
   <plurals name="number_of_items_selected">
-    <item quantity="zero" msgid="2142579311530586258">"Выбрано: %1$d"</item>
-    <item quantity="one" msgid="2478365152745637768">"Выбрано: %1$d"</item>
-    <item quantity="other" msgid="754722656147810487">"Выбрано: %1$d"</item>
+    <item quantity="zero" msgid="2142579311530586258">"%1$d"</item>
+    <item quantity="one" msgid="2478365152745637768">"%1$d"</item>
+    <item quantity="other" msgid="754722656147810487">"%1$d"</item>
   </plurals>
   <plurals name="number_of_albums_selected">
-    <item quantity="zero" msgid="749292746814788132">"Выбрано: %1$d"</item>
-    <item quantity="one" msgid="6184377003099987825">"Выбрано: %1$d"</item>
-    <item quantity="other" msgid="53105607141906130">"Выбрано: %1$d"</item>
+    <item quantity="zero" msgid="749292746814788132">"%1$d"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d"</item>
   </plurals>
   <plurals name="number_of_groups_selected">
-    <item quantity="zero" msgid="3466388370310869238">"Выбрано: %1$d"</item>
-    <item quantity="one" msgid="5030162638216034260">"Выбрано: %1$d"</item>
-    <item quantity="other" msgid="3512041363942842738">"Выбрано: %1$d"</item>
+    <item quantity="zero" msgid="3466388370310869238">"%1$d"</item>
+    <item quantity="one" msgid="5030162638216034260">"%1$d"</item>
+    <item quantity="other" msgid="3512041363942842738">"%1$d"</item>
   </plurals>
     <string name="show_on_map" msgid="6157544221201750980">"Показать на карте"</string>
     <string name="rotate_left" msgid="5888273317282539839">"Повернуть влево"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Не удалось загрузить список альбомов. Повторите попытку позже."</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>
@@ -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/strings.xml b/res/values-sk/strings.xml
index c25ea40..b107b91 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Zoznam albumov 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>
@@ -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/strings.xml b/res/values-sl/strings.xml
index 39c7266..40c8787 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Seznama albumov 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>
@@ -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/strings.xml b/res/values-sr/strings.xml
index 341096a..c200f2c 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"Додирните неко лице за почетак."</string>
     <string name="saving_image" msgid="7270334453636349407">"Чување слике…"</string>
     <string name="save_error" msgid="6857408774183654970">"Није могуће сачувати опсечену слику."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Откажи"</string>
     <string name="share" msgid="3619042788254195341">"Дели"</string>
     <string name="select_all" msgid="3403283025220282175">"Изабери све"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Није могуће преузети листу албума. Покушајте поново касније."</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>
@@ -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/strings.xml b/res/values-sv/strings.xml
index 59f8a8f..f61cbaa 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Avbryt"</string>
     <string name="share" msgid="3619042788254195341">"Dela"</string>
     <string name="select_all" msgid="3403283025220282175">"Markera alla"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Det gick inte att hämta albumlistan. 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>
@@ -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/strings.xml b/res/values-sw/strings.xml
index 193e48d..634eb25 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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 +82,10 @@
     <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 programu inayopatikana ili kukamilisha tendo hilo."</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,11 +97,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Haikuweza kupakua orodha ya albamu. 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>
@@ -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/strings.xml b/res/values-th/strings.xml
index 8ee83a4..53024ea 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"แตะใบหน้าเพื่อเริ่มต้น"</string>
     <string name="saving_image" msgid="7270334453636349407">"กำลังบันทึกภาพ..."</string>
     <string name="save_error" msgid="6857408774183654970">"ไม่สามารถบันทึกภาพที่ตัดได้"</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"ยกเลิก"</string>
     <string name="share" msgid="3619042788254195341">"แบ่งปัน"</string>
     <string name="select_all" msgid="3403283025220282175">"เลือกทั้งหมด"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"ไม่สามารถดาวน์โหลดรายการอัลบั้ม โปรดลองอีกครั้งในภายหลัง"</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>
@@ -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/strings.xml b/res/values-tl/strings.xml
index f52540b..46a1a98 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Kanselahin"</string>
     <string name="share" msgid="3619042788254195341">"Ibahagi"</string>
     <string name="select_all" msgid="3403283025220282175">"Piliin lahat"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Hindi ma-download ang listahan ng mga album. 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>
@@ -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..5321e4d 100644
--- a/res/values-tr/photoeditor_strings.xml
+++ b/res/values-tr/photoeditor_strings.xml
@@ -48,7 +48,7 @@
     <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>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index da4d8ff..dab1423 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Albüm listesi 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>
@@ -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/strings.xml b/res/values-uk/strings.xml
index de76648..27aae30 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"Торкніться обличчя, щоб почати."</string>
     <string name="saving_image" msgid="7270334453636349407">"Зберіг-ня фото…"</string>
     <string name="save_error" msgid="6857408774183654970">"Не вдалося зберегти обрізане зображення."</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Скасувати"</string>
     <string name="share" msgid="3619042788254195341">"Надісл."</string>
     <string name="select_all" msgid="3403283025220282175">"Вибрати всі"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Не вдалося завантажити список альбомів. Повторіть спробу пізніше."</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>
@@ -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/strings.xml b/res/values-vi/strings.xml
index d8df856..325b0a8 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -39,13 +40,16 @@
     <string name="select_image" msgid="7841406150484742140">"Chọn ảnh"</string>
     <string name="select_video" msgid="4859510992798615076">"Chọn video"</string>
     <string name="select_item" msgid="2816923896202086390">"Chọn mục"</string>
-    <string name="select_album" msgid="1557063764849434077">"Chọn anbom"</string>
+    <string name="select_album" msgid="1557063764849434077">"Chọn album"</string>
     <string name="select_group" msgid="6744208543323307114">"Chọn nhóm"</string>
     <string name="set_image" msgid="2331476809308010401">"Đặt ảnh làm"</string>
     <string name="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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <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>
@@ -61,9 +65,9 @@
     <item quantity="other" msgid="754722656147810487">"%1$d mục được chọn"</item>
   </plurals>
   <plurals name="number_of_albums_selected">
-    <item quantity="zero" msgid="749292746814788132">"%1$d anbom được chọn"</item>
-    <item quantity="one" msgid="6184377003099987825">"%1$d anbom được chọn"</item>
-    <item quantity="other" msgid="53105607141906130">"%1$d anbom được chọn"</item>
+    <item quantity="zero" msgid="749292746814788132">"%1$d album được chọn"</item>
+    <item quantity="one" msgid="6184377003099987825">"%1$d album được chọn"</item>
+    <item quantity="other" msgid="53105607141906130">"%1$d album được chọn"</item>
   </plurals>
   <plurals name="number_of_groups_selected">
     <item quantity="zero" msgid="3466388370310869238">"%1$d nhóm được chọn"</item>
@@ -85,21 +89,21 @@
     <string name="group_by_time" msgid="9046168567717963573">"Theo thời gian"</string>
     <string name="group_by_tags" msgid="3568731317210676160">"Theo thẻ"</string>
     <string name="group_by_faces" msgid="1566351636227274906">"Theo người"</string>
-    <string name="group_by_album" msgid="1532818636053818958">"Theo anbom"</string>
+    <string name="group_by_album" msgid="1532818636053818958">"Theo album"</string>
     <string name="group_by_size" msgid="153766174950394155">"Theo kích thước"</string>
     <string name="untagged" msgid="7281481064509590402">"Không được gắn thẻ"</string>
     <string name="no_location" msgid="4043624857489331676">"Không có vị trí nào"</string>
     <string name="no_connectivity" msgid="7164037617297293668">"Không thể xác định một số vị trí do sự cố mạng."</string>
-    <string name="sync_album_error" msgid="2218733298953719785">"Không thể tải xuống các ảnh trong anbom 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 anbom. 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="sync_album_set_error" msgid="3250258387046904444">"Không thể tải xuống danh sách album. 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="no_albums_alert" msgid="4111744447491690896">"Không có anbom nào."</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>
@@ -131,14 +135,14 @@
     <string name="flash_on" msgid="7891556231891837284">"Sử dụng flash"</string>
     <string name="flash_off" msgid="1445443413822680010">"Không có flash"</string>
   <plurals name="make_albums_available_offline">
-    <item quantity="one" msgid="2171596356101611086">"Đặt anbom khả dụng ở chế độ ngoại tuyến."</item>
-    <item quantity="other" msgid="4948604338155959389">"Đặt anbom khả dụng ở chế độ ngoại tuyến."</item>
+    <item quantity="one" msgid="2171596356101611086">"Đặt album khả dụng ở chế độ ngoại tuyến."</item>
+    <item quantity="other" msgid="4948604338155959389">"Đặt album khả dụng ở chế độ ngoại tuyến."</item>
   </plurals>
     <string name="try_to_set_local_album_available_offline" msgid="2184754031896160755">"Mục này được lưu cục bộ và khả dụng ngoại tuyến."</string>
-    <string name="set_label_all_albums" msgid="4581863582996336783">"Tất cả anbom"</string>
-    <string name="set_label_local_albums" msgid="6698133661656266702">"Anbom cục bộ"</string>
+    <string name="set_label_all_albums" msgid="4581863582996336783">"Tất cả album"</string>
+    <string name="set_label_local_albums" msgid="6698133661656266702">"Album cục bộ"</string>
     <string name="set_label_mtp_devices" msgid="1283513183744896368">"Thiết bị MTP"</string>
-    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Anbom Picasa"</string>
+    <string name="set_label_picasa_albums" msgid="5356258354953935895">"Album Picasa"</string>
     <string name="free_space_format" msgid="8766337315709161215">"<xliff:g id="BYTES">%s</xliff:g> trống"</string>
     <string name="size_below" msgid="2074956730721942260">"<xliff:g id="SIZE">%1$s</xliff:g> trở xuống"</string>
     <string name="size_above" msgid="5324398253474104087">"<xliff:g id="SIZE">%1$s</xliff:g> hoặc cao hơn"</string>
@@ -149,12 +153,12 @@
     <string name="camera_connected" msgid="916021826223448591">"Đã kết nối máy ảnh."</string>
     <string name="camera_disconnected" msgid="2100559901676329496">"Đã ngắt kết nối máy ảnh."</string>
     <string name="click_import" msgid="6407959065464291972">"Chạm vào đây để nhập"</string>
-    <string name="widget_type_album" msgid="6013045393140135468">"Chọn anbom"</string>
+    <string name="widget_type_album" msgid="6013045393140135468">"Chọn album"</string>
     <string name="widget_type_shuffle" msgid="8594622705019763768">"Hiển thị ngẫu nhiên tất cả hình ảnh"</string>
     <string name="widget_type_photo" msgid="6267065337367795355">"Chọn ảnh"</string>
     <string name="widget_type" msgid="1364653978966343448">"Chọn hình ảnh"</string>
     <string name="slideshow_dream_name" msgid="6915963319933437083">"Trình chiếu"</string>
-    <string name="albums" msgid="7320787705180057947">"Anbom"</string>
+    <string name="albums" msgid="7320787705180057947">"Album"</string>
     <string name="times" msgid="2023033894889499219">"Lần"</string>
     <string name="locations" msgid="6649297994083130305">"Vị trí"</string>
     <string name="people" msgid="4114003823747292747">"Mọi người"</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-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index a58e1fe..3115784 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"触摸一张脸开始裁剪。"</string>
     <string name="saving_image" msgid="7270334453636349407">"正在保存照片..."</string>
     <string name="save_error" msgid="6857408774183654970">"无法保存经过裁剪的图片。"</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"取消"</string>
     <string name="share" msgid="3619042788254195341">"分享"</string>
     <string name="select_all" msgid="3403283025220282175">"全选"</string>
@@ -75,7 +79,7 @@
     <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="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,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"无法下载相册列表,请稍后重试。"</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>
@@ -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/strings.xml b/res/values-zh-rTW/strings.xml
index 1a4592f..10d4392 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -29,9 +29,10 @@
     <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="multiface_crop_help" msgid="2554690102655855657">"輕觸臉孔即可開始。"</string>
     <string name="saving_image" msgid="7270334453636349407">"正在儲存相片..."</string>
     <string name="save_error" msgid="6857408774183654970">"無法儲存裁剪的圖片。"</string>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"取消"</string>
     <string name="share" msgid="3619042788254195341">"分享"</string>
     <string name="select_all" msgid="3403283025220282175">"全選"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"無法下載相簿清單,請稍後再試。"</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>
@@ -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/strings.xml b/res/values-zu/strings.xml
index e5a8e57..58cc4af 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -29,9 +29,10 @@
     <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="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>
@@ -45,7 +46,10 @@
     <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>
+    <!-- no translation found for confirm_action (1642211585193899041) -->
+    <skip />
+    <!-- no translation found for confirm (8646870096527848520) -->
+    <skip />
     <string name="cancel" msgid="3637516880917356226">"Khansela"</string>
     <string name="share" msgid="3619042788254195341">"Yabelana"</string>
     <string name="select_all" msgid="3403283025220282175">"Khetha konke"</string>
@@ -90,11 +94,11 @@
     <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="sync_album_set_error" msgid="3250258387046904444">"Ayikwazanga ukulanda uhlu lwama-albhamu. Zama futhi 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>
@@ -122,7 +126,7 @@
     <string name="flash" msgid="2816779031261147723">"Ifuleshi"</string>
     <string name="aperture" msgid="5920657630303915195">"Imbobo"</string>
     <string name="focal_length" msgid="1291383769749877010">"Ubude Befokasi"</string>
-    <string name="white_balance" msgid="1582509289994216078">"Bhalansa ubumhlophe"</string>
+    <string name="white_balance" msgid="1582509289994216078">"Ukulingana kokumhlophe"</string>
     <string name="exposure_time" msgid="3990163680281058826">"Isikhathi esisobala"</string>
     <string name="iso" msgid="5028296664327335940">"i-ISO"</string>
     <string name="unit_mm" msgid="1125768433254329136">"mm"</string>
@@ -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_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..e8bf0f4 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,7 @@
 
     <!-- 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>
     <!-- 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>
@@ -90,7 +94,8 @@
 
     <!-- Details dialog "OK" button. Dismisses dialog. -->
     <string name="delete">Delete</string>
-    <string name="confirm_delete">Delete</string>
+    <string name="confirm_action">Confirm deletion?</string>
+    <string name="confirm">Confirm</string>
     <string name="cancel">Cancel</string>
     <string name="share">Share</string>
 
@@ -200,9 +205,9 @@
     <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>
+    <string name="sync_album_error">Couldn\'t download the photos in this album. 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_set_error">Couldn\'t download the list of albums. 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 +221,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>
@@ -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="general_help_link" translatable="false">http://support.google.com/mobile/?p=gallery_top</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index b26c728..0ef23f9 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,14 +17,17 @@
 <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:colorBackground">@null</item>
         <item name="android:colorBackgroundCacheHint">@null</item>
     </style>
+    <style name="Theme.MovieActivity" parent="Theme.Gallery">
+        <item name="android:windowBackground">@android:color/black</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 d25f60e..09e72c0 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,24 @@
 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 boolean mDisableToggleStatusBar;
 
     private AlertDialog mAlertDialog = null;
     private BroadcastReceiver mMountReceiver = new BroadcastReceiver() {
@@ -54,6 +56,12 @@
     private IntentFilter mMountFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
 
     @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        toggleStatusBarByOrientation();
+    }
+
+    @Override
     protected void onSaveInstanceState(Bundle outState) {
         mGLRootView.lockRenderThread();
         try {
@@ -69,16 +77,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 +92,6 @@
         return ((GalleryApp) getApplication()).getThreadPool();
     }
 
-    public GalleryApp getGalleryApplication() {
-        return (GalleryApp) getApplication();
-    }
-
     public synchronized StateManager getStateManager() {
         if (mStateManager == null) {
             mStateManager = new StateManager(this);
@@ -102,21 +103,12 @@
         return mGLRootView;
     }
 
-    public PositionRepository getPositionRepository() {
-        return mPositionRepository;
-    }
-
     @Override
     public void setContentView(int resId) {
         super.setContentView(resId);
         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();
@@ -186,6 +178,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 +206,49 @@
     }
 
     @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);
+        }
     }
 }
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
index 1563a09..443e2bd 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,12 @@
 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 = 4;
 
     private static final int SCREEN_ON_FLAGS = (
               WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
@@ -56,7 +55,6 @@
         public int requestCode;
         public int resultCode = Activity.RESULT_CANCELED;
         public Intent resultData;
-        ResultEntry next;
     }
 
     private boolean mDestroyed = false;
@@ -143,10 +141,9 @@
                 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();
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..c3b04d6 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,30 +43,30 @@
 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.RelativePosition;
+import com.android.gallery3d.ui.ScreenNailHolder;
 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;
-
 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";
@@ -76,18 +79,18 @@
     private static final int BIT_LOADING_SYNC = 2;
 
     private static final float USER_DISTANCE_METER = 0.3f;
+    private static final boolean TEST_CAMERA_PREVIEW = false;
 
     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 +103,41 @@
     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 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
@@ -147,54 +156,79 @@
         } else if (mSelectionManager.inSelectionMode()) {
             mSelectionManager.leaveSelectionMode();
         } else {
-            mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+            // TODO: fix this regression
+            // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
             super.onBackPressed();
         }
     }
 
     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 {
-            mSelectionManager.toggle(item.getPath());
-            mDetailsSource.findIndex(slotIndex);
-            mAlbumView.invalidate();
+            mAlbumView.setPressedUp();
         }
     }
 
+    private void onSingleTapUp(int slotIndex) {
+        if (!mIsActive) return;
+
+        if (mSelectionManager.inSelectionMode()) {
+            MediaItem item = mAlbumDataAdapter.get(slotIndex);
+            if (item == null) return; // Item not ready yet, ignore the click
+            mSelectionManager.toggle(item.getPath());
+            mSlotView.invalidate();
+        } else {
+            // 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());
+            if (TEST_CAMERA_PREVIEW) {
+                ScreenNailHolder holder = new CameraScreenNailHolder(mActivity);
+                data.putParcelable(PhotoPage.KEY_SCREENNAIL_HOLDER, holder);
+            }
+            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 +252,14 @@
 
     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());
+        mSlotView.invalidate();
     }
 
+    @Override
     public void doCluster(int clusterType) {
         String basePath = mMediaSet.getPath().toString();
         String newPath = FilterUtils.newClusterPath(basePath, clusterType);
@@ -242,25 +272,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,52 +288,32 @@
         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
@@ -326,6 +322,11 @@
         mIsActive = true;
         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();
@@ -349,8 +350,10 @@
         if (mSyncTask != null) {
             mSyncTask.cancel();
             mSyncTask = null;
+            clearLoadingBit(BIT_LOADING_SYNC);
         }
         mActionModeHandler.pause();
+        GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
     }
 
     @Override
@@ -362,26 +365,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 +399,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 +417,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 +424,14 @@
                 }
             });
         }
-        mAlbumView.setSelectionDrawer(mHighlightDrawer);
         mDetailsHelper.show();
     }
 
     private void hideDetails() {
         mShowDetails = false;
         mDetailsHelper.hide();
-        mAlbumView.setSelectionDrawer(mGridDrawer);
-        mAlbumView.invalidate();
+        mAlbumView.setHighlightItemPath(null);
+        mSlotView.invalidate();
     }
 
     @Override
@@ -476,6 +472,16 @@
     @Override
     protected boolean onItemSelected(MenuItem item) {
         switch (item.getItemId()) {
+            case android.R.id.home: {
+                if (mActivity.getStateManager().getStateCount() > 1) {
+                    onBackPressed();
+                } else if (mParentMediaSetString != null) {
+                    Bundle data = new Bundle(getData());
+                    data.putString(AlbumSetPage.KEY_MEDIA_PATH, mParentMediaSetString);
+                    mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
+                }
+                return true;
+            }
             case R.id.action_cancel:
                 mActivity.getStateManager().finishState(this);
                 return true;
@@ -516,18 +522,18 @@
                 // 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();
+                mSlotView.setCenterIndex(mFocusIndex);
+                mSlotView.startRestoringAnimation(mFocusIndex);
                 break;
             }
             case REQUEST_DO_ANIMATION: {
-                startTransition(null);
+                mSlotView.startRisingAnimation();
                 break;
             }
         }
@@ -569,21 +575,26 @@
         ((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) {
+                        Toast.makeText((Context) mActivity, R.string.sync_album_error,
+                                Toast.LENGTH_LONG).show();
+                    }
+                } finally {
+                    root.unlockRenderThread();
                 }
             }
         });
     }
 
     private void setLoadingBit(int loadTaskBit) {
-        if (mLoadingBits == 0) {
+        if (mLoadingBits == 0 && mIsActive) {
             GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
         }
         mLoadingBits |= loadTaskBit;
@@ -591,7 +602,7 @@
 
     private void clearLoadingBit(int loadTaskBit) {
         mLoadingBits &= ~loadTaskBit;
-        if (mLoadingBits == 0) {
+        if (mLoadingBits == 0 && mIsActive) {
             GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
 
             if (mAlbumDataAdapter.size() == 0) {
@@ -610,7 +621,6 @@
 
         @Override
         public void onLoadingFinished() {
-            if (!mIsActive) return;
             clearLoadingBit(BIT_LOADING_RELOAD);
         }
     }
@@ -643,7 +653,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 84%
rename from src/com/android/gallery3d/app/AlbumSetDataAdapter.java
rename to src/com/android/gallery3d/app/AlbumSetDataLoader.java
index 5318a61..819adcc 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,6 +323,8 @@
 
         @Override
         public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
             boolean updateComplete = false;
             while (mActive) {
                 synchronized (this) {
@@ -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..f8274ca 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,19 +42,16 @@
 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;
 
@@ -62,6 +61,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 +75,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 +97,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 +114,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 +164,104 @@
         } 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());
+        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,17 +274,27 @@
         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) {
+        if (mLoadingBits == 0 && mIsActive) {
             GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
 
             // Only show toast when there's no album and we are going to finish
@@ -308,7 +310,7 @@
     }
 
     private void setLoadingBit(int loadingBit) {
-        if (mLoadingBits == 0) {
+        if (mLoadingBits == 0 && mIsActive) {
             GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
         }
         mLoadingBits |= loadingBit;
@@ -323,13 +325,16 @@
         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;
             clearLoadingBit(BIT_LOADING_SYNC);
         }
+        GalleryUtils.setSpinnerVisibility((Activity) mActivity, false);
     }
 
     @Override
@@ -345,9 +350,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 +363,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 +372,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 +402,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 +428,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 +439,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 +447,14 @@
                 }
             }
 
-            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));
             }
 
-            actionBar.setTitle(mTitle);
-            actionBar.setSubtitle(mSubtitle);
+            mActionBar.setTitle(mTitle);
+            mActionBar.setSubtitle(mSubtitle);
         }
         return true;
     }
@@ -507,6 +507,11 @@
                 activity.startActivity(new Intent(activity, GallerySettings.class));
                 return true;
             }
+            case R.id.action_general_help: {
+                activity.startActivity(
+                        GalleryUtils.getHelpIntent(R.string.general_help_link, activity));
+                return true;
+            }
             default:
                 return false;
         }
@@ -516,31 +521,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 +536,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,7 +548,7 @@
             case SelectionManager.LEAVE_SELECTION_MODE: {
                 mActionMode.finish();
                 if (mShowClusterMenu) {
-                    mActivity.getGalleryActionBar().showClusterMenu(mSelectedAction, this);
+                    mActionBar.enableClusterMenu(mSelectedAction, this);
                 }
                 mRootPane.invalidate();
                 break;
@@ -573,6 +561,7 @@
         }
     }
 
+    @Override
     public void onSelectionChange(Path path, boolean selected) {
         Utils.assertTrue(mActionMode != null);
         mActionModeHandler.setTitle(getSelectedString());
@@ -582,23 +571,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 +598,52 @@
         ((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) {
+                        Toast.makeText((Context) mActivity, R.string.sync_album_set_error,
+                                Toast.LENGTH_LONG).show();
+                    }
+                } 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 +656,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/CameraScreenNail.java b/src/com/android/gallery3d/app/CameraScreenNail.java
new file mode 100644
index 0000000..24857a4
--- /dev/null
+++ b/src/com/android/gallery3d/app/CameraScreenNail.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import android.app.Activity;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.view.Surface;
+
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.ScreenNailHolder;
+import com.android.gallery3d.ui.SurfaceTextureScreenNail;
+
+// This is a ScreenNail which displays camera preview. This demos the usage of
+// SurfaceTextureScreenNail. It is not intended for production use.
+class CameraScreenNail extends SurfaceTextureScreenNail {
+    private static final String TAG = "CameraScreenNail";
+    private static final int CAMERA_ID = 0;
+    private static final int PREVIEW_WIDTH = 960;
+    private static final int PREVIEW_HEIGHT = 720;
+    private static final int MSG_START_CAMERA = 1;
+    private static final int MSG_STOP_CAMERA = 2;
+
+    public interface Listener {
+        void requestRender();
+    }
+
+    private Activity mActivity;
+    private Listener mListener;
+    private int mOrientation;
+    private Camera mCamera;
+
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private volatile boolean mVisible;
+    private volatile boolean mHasFrame;
+
+    public CameraScreenNail(Activity activity, Listener listener) {
+        mActivity = activity;
+        mListener = listener;
+
+        mOrientation = getCameraDisplayOrientation(mActivity, CAMERA_ID);
+        if (mOrientation % 180 == 0) {
+            setSize(PREVIEW_WIDTH, PREVIEW_HEIGHT);
+        } else {
+            setSize(PREVIEW_HEIGHT, PREVIEW_WIDTH);
+        }
+
+        mHandlerThread = new HandlerThread("Camera");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper()) {
+                public void handleMessage(Message message) {
+                    if (message.what == MSG_START_CAMERA && mCamera == null) {
+                        startCamera();
+                    } else if (message.what == MSG_STOP_CAMERA && mCamera != null) {
+                        stopCamera();
+                    }
+                }
+            };
+        mHandler.sendEmptyMessage(MSG_START_CAMERA);
+    }
+
+    private void startCamera() {
+        try {
+            acquireSurfaceTexture();
+            Camera camera = Camera.open(CAMERA_ID);
+            Camera.Parameters param = camera.getParameters();
+            param.setPreviewSize(PREVIEW_WIDTH, PREVIEW_HEIGHT);
+            camera.setParameters(param);
+            camera.setDisplayOrientation(mOrientation);
+            camera.setPreviewTexture(getSurfaceTexture());
+            camera.startPreview();
+            synchronized (this) {
+                mCamera = camera;
+            }
+        } catch (Throwable th) {
+            Log.e(TAG, "cannot open camera", th);
+        }
+    }
+
+    private void stopCamera() {
+        releaseSurfaceTexture();
+        mCamera.stopPreview();
+        mCamera.release();
+        synchronized (this) {
+            mCamera = null;
+            notifyAll();
+        }
+        mHasFrame = false;
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        if (!mVisible) {
+            mVisible = true;
+            // Only send one message when mVisible makes transition from
+            // false to true.
+            mHandler.sendEmptyMessage(MSG_START_CAMERA);
+        }
+
+        if (mVisible && mHasFrame) {
+            super.draw(canvas, x, y, width, height);
+        }
+    }
+
+    @Override
+    public void noDraw() {
+        mVisible = false;
+    }
+
+    @Override
+    public void recycle() {
+        mVisible = false;
+    }
+
+    @Override
+    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+        mHasFrame = true;
+        if (mVisible) {
+            // We need to ask for re-render if the SurfaceTexture receives a new
+            // frame (and we are visible).
+            mListener.requestRender();
+        }
+    }
+
+    public void destroy() {
+        synchronized (this) {
+            mHandler.sendEmptyMessage(MSG_STOP_CAMERA);
+
+            // Wait until camera is closed.
+            while (mCamera != null) {
+                try {
+                    wait();
+                } catch (Exception ex) {
+                    // ignore.
+                }
+            }
+        }
+        mHandlerThread.quit();
+    }
+
+    // The three methods below are copied from Camera.java
+    private static int getCameraDisplayOrientation(
+            Activity activity, int cameraId) {
+        int displayRotation = getDisplayRotation(activity);
+        int displayOrientation = getDisplayOrientation(
+                displayRotation, cameraId);
+        return displayOrientation;
+    }
+
+    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;
+    }
+
+    private static int getDisplayOrientation(int degrees, int cameraId) {
+        // See android.hardware.Camera.setDisplayOrientation for
+        // documentation.
+        Camera.CameraInfo info = new Camera.CameraInfo();
+        Camera.getCameraInfo(cameraId, info);
+        int result;
+        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+            result = (info.orientation + degrees) % 360;
+            result = (360 - result) % 360;  // compensate the mirror
+        } else {  // back-facing
+            result = (info.orientation - degrees + 360) % 360;
+        }
+        return result;
+    }
+}
+
+// This holds a CameraScreenNail, so we can pass it to a PhotoPage.
+class CameraScreenNailHolder extends ScreenNailHolder
+        implements CameraScreenNail.Listener {
+    private static final String TAG = "CameraScreenNailHolder";
+    private GalleryActivity mActivity;
+    private CameraScreenNail mCameraScreenNail;
+
+    public CameraScreenNailHolder(GalleryActivity activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public void requestRender() {
+        mActivity.getGLRoot().requestRender();
+    }
+
+    @Override
+    public ScreenNail attach() {
+        mCameraScreenNail = new CameraScreenNail((Activity) mActivity, this);
+        return mCameraScreenNail;
+    }
+
+    @Override
+    public void detach() {
+        mCameraScreenNail.destroy();
+        mCameraScreenNail = null;
+    }
+}
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..7e1572f 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;
@@ -554,8 +555,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);
@@ -708,7 +708,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 +734,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;
         }
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..c8fbd53 100644
--- a/src/com/android/gallery3d/app/Gallery.java
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -17,12 +17,15 @@
 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;
@@ -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
@@ -60,7 +62,6 @@
         requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
 
         setContentView(R.layout.main);
-        mActionBar = new GalleryActionBar(this);
 
         if (savedInstanceState != null) {
             getStateManager().restoreFromState(savedInstanceState);
@@ -115,7 +116,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,7 +134,6 @@
 
     private void startViewAction(Intent intent) {
         Boolean slideshow = intent.getBooleanExtra(EXTRA_SLIDESHOW, false);
-        getStateManager().setLaunchGalleryOnTop(true);
         if (slideshow) {
             getActionBar().hide();
             DataManager manager = getDataManager();
@@ -165,7 +164,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)) {
@@ -183,6 +181,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());
@@ -196,11 +196,31 @@
                 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());
+
+                // 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 +233,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 +250,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..c99cd60 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) {
@@ -205,6 +213,16 @@
         }).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 +235,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) {
diff --git a/src/com/android/gallery3d/app/GalleryActivity.java b/src/com/android/gallery3d/app/GalleryActivity.java
index 02f2f72..f41811b 100644
--- a/src/com/android/gallery3d/app/GalleryActivity.java
+++ b/src/com/android/gallery3d/app/GalleryActivity.java
@@ -17,12 +17,9 @@
 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();
 }
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..f97958e 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,7 +303,7 @@
                 ManageCachePage.this.onSingleTapUp(slotIndex);
             }
         });
-        mRootPane.addComponent(mAlbumSetView);
+        mRootPane.addComponent(mSlotView);
         initializeFooterViews();
     }
 
@@ -324,24 +315,28 @@
         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 +383,20 @@
         }
     }
 
+    @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) {
     }
-
 }
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
index 099e9f5..288eeb0 100644
--- a/src/com/android/gallery3d/app/MovieActivity.java
+++ b/src/com/android/gallery3d/app/MovieActivity.java
@@ -18,14 +18,17 @@
 
 import android.app.ActionBar;
 import android.app.Activity;
+import android.content.AsyncQueryHandler;
 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,13 +38,19 @@
 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";
+    private static final String KEY_LOGO_BITMAP = "logo-bitmap";
 
     private MoviePlayer mPlayer;
     private boolean mFinishOnCompletion;
@@ -55,7 +64,7 @@
         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);
         Intent intent = getIntent();
         initializeActionBar(intent);
         mFinishOnCompletion = intent.getBooleanExtra(
@@ -85,26 +94,44 @@
     }
 
     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
diff --git a/src/com/android/gallery3d/app/MovieControllerOverlay.java b/src/com/android/gallery3d/app/MovieControllerOverlay.java
index 752213e..92c3049 100644
--- a/src/com/android/gallery3d/app/MovieControllerOverlay.java
+++ b/src/com/android/gallery3d/app/MovieControllerOverlay.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.os.Handler;
 import android.view.Gravity;
@@ -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.
  */
@@ -103,6 +101,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 +115,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 +134,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 +177,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);
   }
@@ -331,9 +333,6 @@
         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);
-
     if (mainView != null) {
       layoutCenteredView(mainView, l, t, r, b);
     }
@@ -386,5 +385,4 @@
     maybeStartHiding();
     listener.onSeekEnd(time);
   }
-
 }
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..096e781 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -16,6 +16,11 @@
 
 package com.android.gallery3d.app;
 
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Handler;
+import android.os.Message;
+
 import com.android.gallery3d.common.BitmapUtils;
 import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.ContentListener;
@@ -24,8 +29,9 @@
 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.PhotoView.ImageData;
+import com.android.gallery3d.ui.ScreenNail;
 import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.ui.TileImageViewAdapter;
 import com.android.gallery3d.util.Future;
@@ -34,11 +40,6 @@
 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 java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -54,16 +55,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.
@@ -128,9 +129,9 @@
     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];
+    // current, and next image. If 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];
 
     private final Handler mMainHandler;
     private final ThreadPool mThreadPool;
@@ -143,9 +144,9 @@
     private int mSize = 0;
     private Path mItemPath;
     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);
     }
 
@@ -164,6 +165,7 @@
         mItemPath = Utils.checkNotNull(itemPath);
         mCurrentIndex = indexHint;
         mThreadPool = activity.getThreadPool();
+        mNeedFullImage = true;
 
         Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
 
@@ -183,6 +185,10 @@
                         if (mDataListener != null) mDataListener.onLoadingFinished();
                         return;
                     }
+                    case MSG_UPDATE_IMAGE_REQUESTS: {
+                        updateImageRequests();
+                        return;
+                    }
                     default: throw new AssertionError();
                 }
             }
@@ -192,7 +198,7 @@
     }
 
     private long getVersion(int index) {
-        if (index < 0 || index >= mSize) return VERSION_OUT_OF_RANGE;
+        if (index < 0 || index >= mSize) return MediaObject.INVALID_DATA_VERSION;
         if (index >= mContentStart && index < mContentEnd) {
             MediaItem item = mData[index % DATA_CACHE_SIZE];
             if (item != null) return item.getDataVersion();
@@ -200,43 +206,40 @@
         return MediaObject.INVALID_DATA_VERSION;
     }
 
-    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 void fireDataChange() {
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            mChanges[i + SCREEN_NAIL_MAX] = getVersion(mCurrentIndex + i);
         }
+        mPhotoView.notifyDataChange(mChanges, -mCurrentIndex,
+                mSize - 1 - mCurrentIndex);
     }
 
     public void setDataListener(DataListener listener) {
         mDataListener = listener;
     }
 
-    private void updateScreenNail(long version, Future<Bitmap> future) {
+    private void updateScreenNail(long version, Future<ScreenNail> future) {
         ImageEntry entry = mImageCache.get(version);
+        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();
+        Utils.assertTrue(entry.screenNail == null);
+        entry.screenNail = screenNail;
 
-        if (entry.screenNail == null) {
+        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);
-                }
+        }
+
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            if (version == getVersion(mCurrentIndex + i)) {
+                if (i == 0) updateTileProvider(entry);
+                mPhotoView.notifyImageChange(i);
+                break;
             }
         }
         updateImageRequests();
@@ -253,12 +256,9 @@
         entry.fullImageTask = null;
         entry.fullImage = future.get();
         if (entry.fullImage != null) {
-            if (mDataListener != null) {
-                mDataListener.onPhotoAvailable(version, true);
-            }
             if (version == getVersion(mCurrentIndex)) {
                 updateTileProvider(entry);
-                mPhotoView.notifyImageInvalidated(0);
+                mPhotoView.notifyImageChange(0);
             }
         }
         updateImageRequests();
@@ -273,7 +273,7 @@
         mReloadTask = new ReloadTask();
         mReloadTask.start();
 
-        mPhotoView.notifyModelInvalidated();
+        fireDataChange();
     }
 
     public void pause() {
@@ -287,30 +287,28 @@
         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 ScreenNail getImage(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);
+        return entry == null ? null : entry.screenNail;
+    }
+
+    private MediaItem getItem(int index) {
+        if (index < 0 || index >= mSize || !mIsActive) return null;
+        Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+        if (index >= mContentStart && index < mContentEnd) {
+            return mData[index % DATA_CACHE_SIZE];
         }
-    }
-
-    public ImageData getPreviousImage() {
-        return getImage(mCurrentIndex - 1);
-    }
-
-    public ImageData getNextImage() {
-        return getImage(mCurrentIndex + 1);
+        return null;
     }
 
     private void updateCurrentIndex(int index) {
@@ -323,29 +321,55 @@
         updateImageCache();
         updateImageRequests();
         updateTileProvider();
-        mPhotoView.notifyOnNewImage();
 
         if (mDataListener != null) {
             mDataListener.onPhotoChanged(index, mItemPath);
         }
-        fireModelInvalidated();
+
+        fireDataChange();
     }
 
+    @Override
     public void next() {
         updateCurrentIndex(mCurrentIndex + 1);
     }
 
+    @Override
     public void previous() {
         updateCurrentIndex(mCurrentIndex - 1);
     }
 
-    public void jumpTo(int index) {
-        if (mCurrentIndex == index) return;
-        updateCurrentIndex(index);
+    @Override
+    public ScreenNail getScreenNail(int offset) {
+        return getImage(mCurrentIndex + offset);
     }
 
-    public Bitmap getBackupImage() {
-        return mTileProvider.getBackupImage();
+    @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);
+    }
+
+    public ScreenNail getScreenNail() {
+        return mTileProvider.getScreenNail();
     }
 
     public int getImageHeight() {
@@ -356,11 +380,6 @@
         return mTileProvider.getImageWidth();
     }
 
-    public int getImageRotation() {
-        ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
-        return entry == null ? 0 : entry.rotation;
-    }
-
     public int getLevelCount() {
         return mTileProvider.getLevelCount();
     }
@@ -392,7 +411,7 @@
         mCurrentIndex = indexHint;
         updateSlidingWindow();
         updateImageCache();
-        fireModelInvalidated();
+        fireDataChange();
 
         // We need to reload content if the path doesn't match.
         MediaItem item = getCurrentMediaItem();
@@ -411,17 +430,17 @@
     }
 
     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();
@@ -472,6 +491,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;
         }
@@ -491,7 +511,7 @@
         }
     }
 
-    private static class ScreenNailJob implements Job<Bitmap> {
+    private static class ScreenNailJob implements Job<ScreenNail> {
         private MediaItem mItem;
 
         public ScreenNailJob(MediaItem item) {
@@ -499,14 +519,19 @@
         }
 
         @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;
+
             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 new BitmapScreenNail(bitmap);
         }
     }
 
@@ -580,6 +605,7 @@
             ImageEntry entry = mImageCache.remove(version);
             if (entry.fullImageTask != null) entry.fullImageTask.cancel();
             if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+            if (entry.screenNail != null) entry.screenNail.recycle();
         }
     }
 
@@ -606,16 +632,16 @@
     }
 
     private class ScreenNailListener
-            implements Runnable, FutureListener<Bitmap> {
+            implements Runnable, FutureListener<ScreenNail> {
         private final long mVersion;
-        private Future<Bitmap> mFuture;
+        private Future<ScreenNail> mFuture;
 
         public ScreenNailListener(long version) {
             mVersion = version;
         }
 
         @Override
-        public void onFutureDone(Future<Bitmap> future) {
+        public void onFutureDone(Future<ScreenNail> future) {
             mFuture = future;
             mMainHandler.sendMessage(
                     mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
@@ -631,8 +657,8 @@
         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 boolean failToLoad = false;
     }
@@ -738,7 +764,7 @@
             updateImageCache();
             updateTileProvider();
             updateImageRequests();
-            fireModelInvalidated();
+            fireDataChange();
             return null;
         }
 
@@ -746,12 +772,8 @@
             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);
             }
+            fireDataChange();
         }
     }
 
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index ed67b08..6617ce6 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -16,13 +16,17 @@
 
 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.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
@@ -31,6 +35,7 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.ShareActionProvider;
 import android.widget.Toast;
@@ -43,26 +48,25 @@
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.MtpDevice;
 import com.android.gallery3d.data.Path;
+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.GLView;
 import com.android.gallery3d.ui.ImportCompleteListener;
 import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.ScreenNailHolder;
 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;
 
 public class PhotoPage extends ActivityState
-        implements PhotoView.PhotoTapListener, FilmStripView.Listener,
-        UserInteractionListener {
+        implements PhotoView.PhotoTapListener, UserInteractionListener {
     private static final String TAG = "PhotoPage";
 
     private static final int MSG_HIDE_BARS = 1;
@@ -76,13 +80,14 @@
     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_SCREENNAIL_HOLDER = "screennail-holder";
 
     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;
@@ -96,14 +101,20 @@
     private int mCurrentIndex = 0;
     private Handler mHandler;
     private boolean mShowBars = true;
-    private ActionBar mActionBar;
+    private GalleryActionBar mActionBar;
     private MyMenuVisibilityListener mMenuVisibilityListener;
+    private PageTapListener mPageTapListener;
     private boolean mIsMenuVisible;
     private boolean mIsInteracting;
     private MediaItem mCurrentPhoto = null;
     private MenuExecutor mMenuExecutor;
     private boolean mIsActive;
     private ShareActionProvider mShareActionProvider;
+    private String mSetPathString;
+    private ScreenNailHolder mScreenNailHolder;
+    private ScreenNail mScreenNail;
+
+    private NfcAdapter mNfcAdapter;
 
     public static interface Model extends PhotoView.Model {
         public void resume();
@@ -121,6 +132,15 @@
         }
     }
 
+    public interface PageTapListener {
+        // Return true if the tap is consumed.
+        public boolean onSingleTapUp(int x, int y);
+    }
+
+    public void setPageTapListener(PageTapListener listener) {
+        mPageTapListener = listener;
+    }
+
     private final GLView mRootPane = new GLView() {
 
         @Override
@@ -132,42 +152,15 @@
         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);
 
@@ -176,16 +169,35 @@
         mRootPane.addComponent(mPhotoView);
         mApplication = (GalleryApp)((Activity) mActivity).getApplication();
 
-        String setPathString = data.getString(KEY_MEDIA_SET_PATH);
+        mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
+        mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
         Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
 
-        if (setPathString != null) {
-            mMediaSet = mActivity.getDataManager().getMediaSet(setPathString);
+        if (mSetPathString != null) {
+            mScreenNailHolder =
+                (ScreenNailHolder) data.getParcelable(KEY_SCREENNAIL_HOLDER);
+            if (mScreenNailHolder != null) {
+                mScreenNail = mScreenNailHolder.attach();
+
+                // Get the ScreenNail from ScreenNailHolder and register it.
+                int id = SnailSource.registerScreenNail(mScreenNail);
+                Path screenNailSetPath = SnailSource.getSetPath(id);
+                Path screenNailItemPath = SnailSource.getItemPath(id);
+
+                // Combine the original MediaSet with the one for CameraScreenNail.
+                mSetPathString = "/combo/item/{" + screenNailSetPath +
+                        "," + mSetPathString + "}";
+
+                // Start from the screen nail.
+                itemPath = screenNailItemPath;
+            }
+
+            mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString);
             mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
             mMediaSet = (MediaSet)
-                    mActivity.getDataManager().getMediaObject(setPathString);
+                    mActivity.getDataManager().getMediaObject(mSetPathString);
             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);
@@ -199,7 +211,6 @@
 
                 @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) {
@@ -227,11 +238,6 @@
                 public void onLoadingStarted() {
                     GalleryUtils.setSpinnerVisibility((Activity) mActivity, true);
                 }
-
-                @Override
-                public void onPhotoAvailable(long version, boolean fullImage) {
-                    if (mFilmStripView == null) initFilmStripView();
-                }
             });
         } else {
             // Get default media set by the URI
@@ -255,8 +261,10 @@
             }
         };
 
-        // 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 +275,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 +286,31 @@
         }
     }
 
-    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);
 
-        updateShareURI(photo.getPath());
+        if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
+            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() {
@@ -334,9 +348,6 @@
         WindowManager.LayoutParams params = ((Activity) mActivity).getWindow().getAttributes();
         params.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE;
         ((Activity) mActivity).getWindow().setAttributes(params);
-        if (mFilmStripView != null) {
-            mFilmStripView.show();
-        }
     }
 
     private void hideBars() {
@@ -346,9 +357,6 @@
         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();
-        }
     }
 
     private void refreshHidingMessage() {
@@ -395,22 +403,18 @@
     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 (!switchWithCaptureAnimation(-1)) {
             super.onBackPressed();
         }
     }
 
+    // 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) {
+        return mPhotoView.switchWithCaptureAnimation(offset);
+    }
+
     @Override
     protected boolean onCreateActionBar(Menu menu) {
         MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
@@ -420,6 +424,7 @@
         mMenu = menu;
         mShowBars = true;
         updateMenuOperations();
+        updateTitle();
         return true;
     }
 
@@ -437,7 +442,24 @@
 
         DataManager manager = mActivity.getDataManager();
         int action = item.getItemId();
+        boolean needsConfirm = false;
         switch (action) {
+            case android.R.id.home: {
+                if (mSetPathString != null) {
+                    if (mActivity.getStateManager().getStateCount() > 1) {
+                        onBackPressed();
+                    } else {
+                        // We're in view mode so set up the stacks on our own.
+                        Bundle data = new Bundle(getData());
+                        data.putString(AlbumPage.KEY_MEDIA_PATH, mSetPathString);
+                        data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+                                mActivity.getDataManager().getTopSetPath(
+                                        DataManager.INCLUDE_ALL));
+                        mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+                    }
+                }
+                return true;
+            }
             case R.id.action_slideshow: {
                 Bundle data = new Bundle();
                 data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
@@ -466,20 +488,21 @@
                 }
                 return true;
             }
+            case R.id.action_delete:
+                needsConfirm = true;
             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, needsConfirm, null);
                 return true;
             case R.id.action_import:
                 mSelectionManager.deSelectAll();
                 mSelectionManager.toggle(path);
-                mMenuExecutor.onMenuClicked(item,
+                mMenuExecutor.onMenuClicked(item, needsConfirm,
                         new ImportCompleteListener(mActivity));
                 return true;
             default :
@@ -507,6 +530,10 @@
     }
 
     public void onSingleTapUp(int x, int y) {
+        if (mPageTapListener != null) {
+            if (mPageTapListener.onSingleTapUp(x, y)) return;
+        }
+
         MediaItem item = mModel.getCurrentMediaItem();
         if (item == null) {
             // item is not ready, ignore
@@ -544,12 +571,6 @@
         }
     }
 
-    // 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);
-    }
-
     @Override
     protected void onStateResult(int requestCode, int resultCode, Intent data) {
         switch (requestCode) {
@@ -586,14 +607,12 @@
     public void onPause() {
         super.onPause();
         mIsActive = false;
-        if (mFilmStripView != null) {
-            mFilmStripView.pause();
-        }
         DetailsHelper.pause();
         mPhotoView.pause();
         mModel.pause();
         mHandler.removeMessages(MSG_HIDE_BARS);
         mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
+
         mMenuExecutor.pause();
     }
 
@@ -602,18 +621,30 @@
         super.onResume();
         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();
     }
 
+    @Override
+    protected void onDestroy() {
+        if (mScreenNailHolder != null) {
+            // Unregister the ScreenNail and notify mScreenNailHolder.
+            SnailSource.unregisterScreenNail(mScreenNail);
+            mScreenNailHolder.detach();
+            mScreenNailHolder = null;
+            mScreenNail = null;
+        }
+        super.onDestroy();
+    }
+
     private class MyDetailsSource implements DetailsSource {
         private int mIndex;
 
@@ -638,4 +669,5 @@
             return mIndex;
         }
     }
+
 }
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..47f6acb 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 {
 
@@ -109,16 +109,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);
         }
@@ -128,9 +124,8 @@
         try {
             Bitmap backup = future.get();
             if (backup == null) return;
-            setBackupImage(backup, backup.getWidth(), backup.getHeight());
-            mPhotoView.notifyOnNewImage();
-            mPhotoView.notifyImageInvalidated(0); // the current image
+            setScreenNail(backup, backup.getWidth(), backup.getHeight());
+            mPhotoView.notifyImageChange(0);
         } catch (Throwable t) {
             Log.w(TAG, "fail to decode thumb", t);
         }
@@ -158,26 +153,43 @@
         }
     }
 
-    public ImageData getNextImage() {
-        return null;
-    }
-
-    public ImageData getPreviousImage() {
-        return null;
-    }
-
+    @Override
     public void next() {
         throw new UnsupportedOperationException();
     }
 
+    @Override
     public void previous() {
         throw new UnsupportedOperationException();
     }
 
-    public void jumpTo(int index) {
-        throw new UnsupportedOperationException();
+    @Override
+    public void getImageSize(int offset, PhotoView.Size size) {
+        if (offset == 0) {
+            size.width = mItem.getWidth();
+            size.height = mItem.getHeight();
+        } else {
+            size.width = 0;
+            size.height = 0;
+        }
     }
 
+    @Override
+    public int getImageRotation(int offset) {
+        return (offset == 0) ? mItem.getFullImageRotation() : 0;
+    }
+
+    @Override
+    public ScreenNail getScreenNail(int offset) {
+        return (offset == 0) ? getScreenNail() : null;
+    }
+
+    @Override
+    public void setNeedFullImage(boolean enabled) {
+        // currently not necessary.
+    }
+
+
     public MediaItem getCurrentMediaItem() {
         return mItem;
     }
diff --git a/src/com/android/gallery3d/app/SlideshowDream.java b/src/com/android/gallery3d/app/SlideshowDream.java
index 80f7fe0..c249467 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);
diff --git a/src/com/android/gallery3d/app/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java
index 8697629..5aa3985 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;
diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java
index 556a06a..5866be9 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);
@@ -135,18 +129,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 +147,7 @@
     }
 
     void finishState(ActivityState state) {
-        Log.v(TAG, "finishState " + state.getClass());
+        Log.v(TAG, "finishState " + state);
         if (state != mStack.peek().activityState) {
             if (state.isDestroyed()) {
                 Log.d(TAG, "The state is already destroyed");
@@ -235,7 +223,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 +248,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/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/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
index f7dac5e..1da3b76 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,14 +242,6 @@
         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) {
         if (uri == null) return null;
         for (MediaSource source : mSourceMap.values()) {
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..dbb5189 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,15 @@
     private final ChangeNotifier mNotifierImage;
     private final ChangeNotifier mNotifierVideo;
     private final String mName;
+    private final Handler mHandler;
+
+    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 +120,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 +138,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 +166,9 @@
                         buffer.add(entry);
                     }
                 }
+                if (jc.isCancelled()) return null;
             }
+            Log.v("DebugLoadingTime", "got " + buffer.size() + " buckets");
         } finally {
             cursor.close();
         }
@@ -166,62 +183,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 +261,39 @@
     }
 
     @Override
-    public long reload() {
+    // 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();
+            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();
+        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..4f2797e 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;
 
@@ -159,14 +159,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 +182,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 +206,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..2749ebe 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;
 
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..19b2fec 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;
 
@@ -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..0ba59f5 100644
--- a/src/com/android/gallery3d/data/LocalVideo.java
+++ b/src/com/android/gallery3d/data/LocalVideo.java
@@ -16,26 +16,20 @@
 
 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 {
@@ -73,7 +67,6 @@
     };
 
     private final GalleryApp mApplication;
-    private static Bitmap sOverlay;
 
     public int durationInSec;
 
@@ -148,7 +141,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;
         }
 
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/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java
index ae98e0f..07479a7 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 {
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..ee5cf7e 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,9 @@
 import android.util.Log;
 import android.widget.Toast;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.GalleryUtils;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
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..6dcb0d2 100644
--- a/src/com/android/gallery3d/data/MtpDeviceSet.java
+++ b/src/com/android/gallery3d/data/MtpDeviceSet.java
@@ -16,27 +16,37 @@
 
 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;
 
     public MtpDeviceSet(Path path, GalleryApp application, MtpContext mtpContext) {
         super(path, nextVersionNumber());
@@ -44,28 +54,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 +113,33 @@
     }
 
     @Override
-    public long reload() {
+    public synchronized long reload() {
         if (mNotifier.isDirty()) {
+            if (mLoadTask != null) mLoadTask.cancel();
+            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();
+        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..39467bb
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailAlbum.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import java.util.ArrayList;
+
+// This is a simple MediaSet which contains only one MediaItem -- a SnailItem.
+public class SnailAlbum extends MediaSet {
+
+    private MediaItem mItem;
+
+    public SnailAlbum(Path path, MediaItem item) {
+        super(path, nextVersionNumber());
+        mItem = 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() {
+        return mDataVersion;
+    }
+}
diff --git a/src/com/android/gallery3d/data/SnailItem.java b/src/com/android/gallery3d/data/SnailItem.java
new file mode 100644
index 0000000..2836a19
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailItem.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 ScreenNail mScreenNail;
+
+    public SnailItem(Path path, ScreenNail screenNail) {
+        super(path, nextVersionNumber());
+        mScreenNail = screenNail;
+    }
+
+    @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;
+    }
+}
diff --git a/src/com/android/gallery3d/data/SnailSource.java b/src/com/android/gallery3d/data/SnailSource.java
new file mode 100644
index 0000000..e74a8bb
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailSource.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.data;
+
+import android.util.SparseArray;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.ui.ScreenNail;
+
+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;
+    private static SparseArray<ScreenNail> sRegistry = new SparseArray<ScreenNail>();
+
+    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, lookupScreenNail(id));
+            }
+        }
+        return null;
+    }
+
+    // Registers a ScreenNail and returns the id of it. You can obtain the Path
+    // of the MediaItem associated with the ScreenNail by getItemPath(), and the
+    // Path of the MediaSet containing that MediaItem by getSetPath().
+    public static synchronized int registerScreenNail(ScreenNail s) {
+        int id = sNextId++;
+        sRegistry.put(id, s);
+        return id;
+    }
+
+    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);
+    }
+
+    public static synchronized void unregisterScreenNail(ScreenNail s) {
+        int index = sRegistry.indexOfValue(s);
+        sRegistry.removeAt(index);
+    }
+
+    private static synchronized ScreenNail lookupScreenNail(int id) {
+        return sRegistry.get(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..05850bb 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;
@@ -32,6 +25,13 @@
 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;
 import java.io.InputStream;
@@ -180,7 +180,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 +195,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..ac53dd5 100644
--- a/src/com/android/gallery3d/data/UriSource.java
+++ b/src/com/android/gallery3d/data/UriSource.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.net.URLDecoder;
 import java.net.URLEncoder;
 
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..36575e4 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,9 @@
 import android.util.Log;
 import android.widget.Toast;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+
 public class WidgetClickHandler extends Activity {
     private static final String TAG = "PhotoAppWidgetClickHandler";
 
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/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/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..19e49c4 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);
                                     }
@@ -223,7 +223,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..6b7b7c3 100644
--- a/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
+++ b/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
@@ -48,29 +48,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 File saveDirectory;
 
     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()));
     }
@@ -85,7 +80,9 @@
             return null;
         }
         Bitmap bitmap = params[0];
-        File file = save(bitmap);
+        getSaveDirectory();
+        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 +91,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, saveDirectory.getName());
         Toast toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
         toast.setGravity(Gravity.CENTER, 0, 0);
         toast.show();
@@ -102,10 +99,36 @@
         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 void getSaveDirectory() {
+        querySource(new String[] { ImageColumns.DATA }, new ContentResolverQueryCallback () {
+
+            @Override
+            public void onCursorResult(Cursor cursor) {
+                saveDirectory = new File(cursor.getString(0)).getParentFile();
+            }
+        });
+        // Use the default save directory if the source directory cannot be saved.
+        if ((saveDirectory == null) || !saveDirectory.canWrite()) {
+            saveDirectory = new File(Environment.getExternalStorageDirectory(),
+                    context.getString(R.string.edited_photo_bucket_name));
+        }
     }
 
     /**
@@ -113,43 +136,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/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..0188393 100644
--- a/src/com/android/gallery3d/ui/ActionModeHandler.java
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -28,8 +28,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;
@@ -88,6 +88,7 @@
                 R.menu.selection);
         updateSelectionMenu();
         customMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+            @Override
             public boolean onMenuItemClick(MenuItem item) {
                 return onActionItemClicked(actionMode, item);
             }
@@ -103,25 +104,40 @@
         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;
+            boolean needsConfirm = false;
+            int action = item.getItemId();
+            if (action == R.id.action_import) {
+                listener = new ImportCompleteListener(mActivity);
+            } else if (item.getItemId() == R.id.action_delete) {
+                needsConfirm = true;
+            }
+            mMenuExecutor.onMenuClicked(item, needsConfirm, 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 +189,48 @@
     // 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) 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 +250,9 @@
                 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);
-                }
-            });
         }
+
+        return intent;
     }
 
     public void updateSupportedOperation(Path path, boolean selected) {
@@ -258,21 +261,38 @@
     }
 
     public void updateSupportedOperation() {
+        // Interrupt previous unfinished task, mMenuTask is only accessed in main thread
         if (mMenuTask != null) {
             mMenuTask.cancel();
         }
 
         // 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..f837092
--- /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() {
+        mBitmapPool.clear();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
index 87ff557..7fa7df4 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,70 @@
 
     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 Texture label;
         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 +160,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 +204,136 @@
 
     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();
+        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.label = 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.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.content instanceof BitmapTexture) {
+            mTextureUploader.addBgTexture((BitmapTexture) entry.content);
         }
-
-        prepareSlotContent(slotIndex);
-        AlbumSetItem update = data[pos];
-
-        if (mListener != null && isActiveSlot(slotIndex)) {
-            mListener.onWindowContentChanged(slotIndex, original, update);
+        if (entry.label instanceof BitmapTexture) {
+            mTextureUploader.addBgTexture((BitmapTexture) entry.label);
         }
-        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.content instanceof BitmapTexture) {
+                mTextureUploader.addFgTexture((BitmapTexture) entry.content);
+            }
+            if (entry.label instanceof BitmapTexture) {
+                mTextureUploader.addFgTexture((BitmapTexture) entry.label);
             }
         }
-    }
 
-    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 +342,114 @@
         }
     }
 
-    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;
+        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.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 +470,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.label = 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.label = 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..b0e6153
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
@@ -0,0 +1,227 @@
+/*
+ * 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(GLCanvas canvas, Texture texture) {
+        return ((texture == null) || ((texture instanceof UploadedTexture)
+                && !((UploadedTexture) texture).isContentValid(canvas)))
+                ? 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(canvas, entry.content);
+        if (content == null) {
+            content = mWaitLoadingTexture;
+            entry.isWaitLoadingDisplayed = true;
+        } else if (entry.isWaitLoadingDisplayed) {
+            entry.isWaitLoadingDisplayed = false;
+            entry.content = new FadeInTexture(
+                    PLACEHOLDER_COLOR, (BitmapTexture) entry.content);
+            content = entry.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) {
+        // We show the loading message only when the album is still loading
+        // (Not when we are still preparing the label)
+        Texture content = checkTexture(canvas, entry.label);
+        if (entry.album == null) {
+            content = mDataWindow.getLoadingTexture();
+        }
+        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..bc6f738 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,36 @@
 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 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 +67,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 +145,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 +157,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.content instanceof BitmapTexture) {
+                mTextureUploader.addBgTexture((BitmapTexture) entry.content);
+            }
+        }
+    }
+
+    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.content instanceof BitmapTexture) {
+                mTextureUploader.addFgTexture((BitmapTexture) entry.content);
+            }
+        }
+
+        // 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 +200,64 @@
         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();
         }
+        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,68 @@
         }
     }
 
-    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.content = new BitmapTexture(bitmap);
+
+            if (isActiveSlot(mSlotIndex)) {
+                mTextureUploader.addFgTexture((BitmapTexture) entry.content);
                 --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((BitmapTexture) entry.content);
             }
         }
-
-        // 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();
+            }
         }
     }
 
@@ -440,6 +344,5 @@
         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..922e2c3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
@@ -0,0 +1,186 @@
+/*
+ * 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 {
+    private static final int PLACEHOLDER_COLOR = 0xFF222222;
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "AlbumView";
+    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;
+
+    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(GLCanvas canvas, Texture texture) {
+        return ((texture == null) || ((texture instanceof UploadedTexture)
+                && !((UploadedTexture) texture).isContentValid(canvas)))
+                ? null
+                : texture;
+    }
+
+    @Override
+    public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+        AlbumSlidingWindow.AlbumEntry entry = mDataWindow.get(index);
+
+        int renderRequestFlags = 0;
+
+        Texture content = checkTexture(canvas, entry.content);
+        if (content == null) {
+            content = mWaitLoadingTexture;
+            entry.isWaitDisplayed = true;
+        } else if (entry.isWaitDisplayed) {
+            entry.isWaitDisplayed = false;
+            entry.content = new FadeInTexture(
+                    PLACEHOLDER_COLOR, (BitmapTexture) entry.content);
+            content = entry.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
+    }
+}
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..6a9a17d 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.
@@ -45,7 +44,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 +63,7 @@
     }
 
     protected void setAssociatedCanvas(GLCanvas canvas) {
-        mCanvasRef = canvas == null
-                ? null
-                : new WeakReference<GLCanvas>(canvas);
+        mCanvasRef = canvas;
     }
 
     /**
@@ -133,8 +130,11 @@
     // It should make sure the data is uploaded to GL memory.
     abstract protected boolean onBind(GLCanvas canvas);
 
+    // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
+    abstract protected int getTarget();
+
     public boolean isLoaded(GLCanvas canvas) {
-        return mState == STATE_LOADED && mCanvasRef.get() == canvas;
+        return mState == STATE_LOADED;
     }
 
     // recycle() is called when the texture will never be used again,
@@ -153,7 +153,7 @@
     }
 
     private void freeResource() {
-        GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get();
+        GLCanvas canvas = mCanvasRef;
         if (canvas != null && isLoaded(canvas)) {
             canvas.unloadTexture(this);
         }
@@ -182,4 +182,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..7f65405
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 android.util.Log;
+
+import com.android.gallery3d.data.MediaItem;
+
+// This is a ScreenNail wraps a Bitmap. It also includes the rotation
+// information. The getWidth() and getHeight() methods return the width/height
+// before rotation.
+public class BitmapScreenNail implements ScreenNail {
+    private static final String TAG = "BitmapScreenNail";
+    private final int mWidth;
+    private final int mHeight;
+    private Bitmap mBitmap;
+    private BitmapTexture mTexture;
+
+    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.
+    }
+
+    @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 (mTexture == null) {
+            mTexture = new BitmapTexture(mBitmap);
+        }
+        mTexture.draw(canvas, x, y, width, height);
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        if (mTexture == null) {
+            mTexture = new BitmapTexture(mBitmap);
+        }
+        canvas.drawTexture(mTexture, source, dest);
+    }
+}
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..be05b33 100644
--- a/src/com/android/gallery3d/ui/BitmapTileProvider.java
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java
@@ -16,16 +16,16 @@
 
 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 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,13 +44,13 @@
             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;
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
     }
 
     public int getImageHeight() {
@@ -95,11 +95,9 @@
         for (Bitmap bitmap : mMipmaps) {
             BitmapUtils.recycleSilently(bitmap);
         }
-        BitmapUtils.recycleSilently(mBackup);
-    }
-
-    public int getRotation() {
-        return 0;
+        if (mScreenNail != null) {
+            mScreenNail.recycle();
+        }
     }
 
     public boolean isFailedToLoad() {
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..d8267d1
--- /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 = UploadedTexture.STATE_LOADED;
+    }
+
+    @Override
+    protected boolean onBind(GLCanvas canvas) {
+        if (!isLoaded(canvas)) {
+            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..c438977
--- /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 implements Texture {
+    @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..ad0d358
--- /dev/null
+++ b/src/com/android/gallery3d/ui/FadeTexture.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.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;
+    }
+
+    public void draw(GLCanvas canvas, int x, int y) {
+        draw(canvas, x, y, mWidth, mHeight);
+    }
+
+    /**
+     * Subclasses should implement how to fade the texture.
+     */
+    public abstract void draw(GLCanvas canvas, int x, int y, int w, int 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;
+    }
+
+    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..9b8053f 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,6 @@
     // called in the GL thread.
     public void deleteRecycledResources();
 
+    // Dump statistics information and clear the counters. For debug only.
+    public void dumpStatisticsAndClear();
 }
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
index 612c7c4..f822951 100644
--- a/src/com/android/gallery3d/ui/GLCanvasImpl.java
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -16,18 +16,19 @@
 
 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;
@@ -51,8 +52,9 @@
     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];
 
@@ -60,12 +62,9 @@
 
     private final GLState mGLState;
 
-    private long mAnimationTime;
-
     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();
@@ -107,28 +106,23 @@
         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;
-    }
-
     public void setAlpha(float alpha) {
         Utils.assertTrue(alpha >= 0 && alpha <= 1);
         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 +136,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);
@@ -168,10 +162,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 +179,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 +196,7 @@
         GL11 gl = mGL;
 
         saveTransform();
-        translate(x, y, 0);
+        translate(x, y);
         scale(width, height, 1);
 
         gl.glLoadMatrixf(mMatrixValues, 0);
@@ -218,11 +210,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 +243,7 @@
         GL11 gl = mGL;
 
         saveTransform();
-        translate(x, y, 0);
+        translate(x, y);
         scale(width, height, 1);
 
         gl.glLoadMatrixf(mMatrixValues, 0);
@@ -263,7 +267,7 @@
         setTextureCoords(0, 0, 1, 1);
 
         saveTransform();
-        translate(x, y, 0);
+        translate(x, y);
 
         mGL.glLoadMatrixf(mMatrixValues, 0);
 
@@ -285,55 +289,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 +332,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 +348,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 +377,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 +423,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 +441,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 +459,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 +500,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 +514,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;
@@ -705,16 +565,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 +585,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 +595,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 +622,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,6 +643,12 @@
         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) {
@@ -816,23 +669,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 +694,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 +701,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);
     }
@@ -886,12 +733,6 @@
 
         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);
             }
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..fe040ba 100644
--- a/src/com/android/gallery3d/ui/GLRoot.java
+++ b/src/com/android/gallery3d/ui/GLRoot.java
@@ -20,15 +20,17 @@
 
 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();
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java
index f2140bf..0268ef9 100644
--- a/src/com/android/gallery3d/ui/GLRootView.java
+++ b/src/com/android/gallery3d/ui/GLRootView.java
@@ -16,11 +16,6 @@
 
 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.PixelFormat;
 import android.graphics.Rect;
@@ -28,12 +23,17 @@
 import android.os.Process;
 import android.os.SystemClock;
 import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.view.MotionEvent;
 
+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.ReentrantLock;
+
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 import javax.microedition.khronos.opengles.GL11;
@@ -59,14 +59,16 @@
 
     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 int mFlags = FLAG_NEED_LAYOUT;
     private volatile boolean mRenderRequested = false;
@@ -80,14 +82,13 @@
     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 static final int TARGET_FRAME_TIME = 16;
     private long mLastDrawFinishTime;
     private boolean mInDownState = false;
 
@@ -107,15 +108,6 @@
         //setDebugFlags(DEBUG_CHECK_GL_ERROR);
     }
 
-    public GalleryEGLConfigChooser getEGLConfigChooser() {
-        return mEglConfigChooser;
-    }
-
-    @Override
-    public boolean hasStencil() {
-        return getEGLConfigChooser().getStencilBits() > 0;
-    }
-
     @Override
     public void registerLaunchedAnimation(CanvasAnimation animation) {
         // Register the newly launched animation so that we can set the start
@@ -155,10 +147,6 @@
         }
     }
 
-    public GLView getContentPane() {
-        return mContentView;
-    }
-
     @Override
     public void requestRender() {
         if (DEBUG_INVALIDATE) {
@@ -219,10 +207,10 @@
         }
         mGL = gl;
         mCanvas = new GLCanvasImpl(gl);
-        if (!DEBUG_FPS) {
-            setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
-        } else {
+        if (DEBUG_FPS || DEBUG_PROFILE) {
             setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+        } else {
+            setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
         }
     }
 
@@ -237,6 +225,11 @@
                 + ", gl10: " + gl1.toString());
         Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
         GalleryUtils.setRenderThread();
+        BasicTexture.invalidateAllTextures();
+        if (DEBUG_PROFILE) {
+            Log.d(TAG, "Start profiling");
+            Profile.enable(20);  // take a sample every 20ms
+        }
         GL11 gl = (GL11) gl1;
         Utils.assertTrue(mGL == gl);
 
@@ -261,21 +254,33 @@
 
     @Override
     public void onDrawFrame(GL10 gl) {
+        AnimationTime.update();
+        long t0;
+        if (DEBUG_PROFILE_SLOW_ONLY) {
+            Profile.hold();
+            t0 = System.nanoTime();
+        }
         mRenderLock.lock();
         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);
+        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) {
@@ -300,13 +305,12 @@
             gl.glScissor(clip.left, clip.top, clip.width(), clip.height());
         }
 
-        mCanvas.setCurrentAnimationTimeMillis(SystemClock.uptimeMillis());
         if (mContentView != null) {
            mContentView.render(mCanvas);
         }
 
         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 +322,7 @@
         }
 
         synchronized (mIdleListeners) {
-            if (!mRenderRequested && !mIdleListeners.isEmpty()) {
-                mIdleRunner.enable();
-            }
+            if (!mIdleListeners.isEmpty()) mIdleRunner.enable();
         }
 
         if (DEBUG_INVALIDATE) {
@@ -335,6 +337,7 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent event) {
+        AnimationTime.update();
         int action = event.getAction();
         if (action == MotionEvent.ACTION_CANCEL
                 || action == MotionEvent.ACTION_UP) {
@@ -356,19 +359,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 +368,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 +400,15 @@
     public void unlockRenderThread() {
         mRenderLock.unlock();
     }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        if (DEBUG_PROFILE) {
+            Log.d(TAG, "Stop profiling");
+            Profile.disableAll();
+            Profile.dumpToFile("/sdcard/gallery.prof");
+            Profile.reset();
+        }
+    }
 }
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
index 7491a6f..45471f9 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,14 +303,6 @@
         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) {
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..2450881 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;
 
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..a0f3449 100644
--- a/src/com/android/gallery3d/ui/MenuExecutor.java
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -17,8 +17,11 @@
 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.OnClickListener;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.Message;
@@ -172,10 +175,9 @@
         return ids.get(0);
     }
 
-    public boolean onMenuClicked(MenuItem menuItem, ProgressListener listener) {
+    private void onMenuClicked(int action, ProgressListener listener) {
         int title;
         DataManager manager = mActivity.getDataManager();
-        int action = menuItem.getItemId();
         switch (action) {
             case R.id.action_select_all:
                 if (mSelectionManager.inSelectAllMode()) {
@@ -183,14 +185,12 @@
                 } else {
                     mSelectionManager.selectAll();
                 }
-                return true;
             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);
                 ((Activity) mActivity).startActivity(intent);
-                return true;
             }
             case R.id.action_setas: {
                 Path path = getSingleSelectedPath();
@@ -203,9 +203,8 @@
                 Activity activity = (Activity) mActivity;
                 activity.startActivity(Intent.createChooser(
                         intent, activity.getString(R.string.set_as)));
-                return true;
             }
-            case R.id.action_confirm_delete:
+            case R.id.action_delete:
                 title = R.string.delete;
                 break;
             case R.id.action_rotate_cw:
@@ -224,10 +223,27 @@
                 title = R.string.Import;
                 break;
             default:
-                return false;
+                return;
         }
         startAction(action, title, listener);
-        return true;
+    }
+
+    public void onMenuClicked(MenuItem menuItem, boolean needsConfirm,
+            final ProgressListener listener) {
+        final int action = menuItem.getItemId();
+
+        if (needsConfirm) {
+            new AlertDialog.Builder(mActivity.getAndroidContext())
+                    .setMessage(R.string.confirm_action)
+                    .setPositiveButton(R.string.confirm, new OnClickListener() {
+                            public void onClick(DialogInterface dialog, int which) {
+                                onMenuClicked(action, listener);
+                            }
+                        })
+                    .setNegativeButton(R.string.cancel, null).create().show();
+        } else {
+            onMenuClicked(action, listener);
+        }
     }
 
     public void startAction(int action, int title, ProgressListener listener) {
@@ -257,7 +273,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:
diff --git a/src/com/android/gallery3d/ui/NinePatchTexture.java b/src/com/android/gallery3d/ui/NinePatchTexture.java
index 15b057a..957229e 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);
             }
@@ -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/OnSelectedListener.java
deleted file mode 100644
index 2cc5809..0000000
--- a/src/com/android/gallery3d/ui/OnSelectedListener.java
+++ /dev/null
@@ -1,21 +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 interface OnSelectedListener {
-    public void onSelected(GLView source);
-}
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/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index 217a290..66941be 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -16,12 +16,6 @@
 
 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 android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
@@ -29,47 +23,77 @@
 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.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.RangeBoolArray;
+
+import java.util.Arrays;
+
 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;
+    }
+
+    public interface Model extends TileImageView.Model {
+        public void next();
+        public void previous();
+
+        // Returns the size for the specified picture. If the size information is
+        // not avaiable, width = height = 0.
+        public void getImageSize(int offset, Size size);
+
+        // Returns the rotation for the specified picture.
+        public int getImageRotation(int offset);
+
+        // This amends the getScreenNail() method of TileImageView.Model to get
+        // ScreenNail at previous (negative offset) or next (positive offset)
+        // positions. Returns null if the specified ScreenNail is unavailable.
+        public ScreenNail getScreenNail(int offset);
+
+        // Set this to true if we need the model to provide full images.
+        public void setNeedFullImage(boolean enabled);
+    }
+
+    public interface PhotoTapListener {
+        public void onSingleTapUp(int x, int y);
+    }
+
+    private static final int MSG_SHOW_LOADING = 1;
+    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 long DELAY_SHOW_LOADING = 250; // 250ms;
 
-    private static final int TRANS_NONE = 0;
-    private static final int TRANS_SWITCH_NEXT = 3;
-    private static final int TRANS_SWITCH_PREVIOUS = 4;
-
-    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;
-
     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;
 
-    private static final int ENTRY_PREVIOUS = 0;
-    private static final int ENTRY_NEXT = 1;
-
-    private static final int IMAGE_GAP = 96;
-    private static final int SWITCH_THRESHOLD = 256;
+    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;
 
+    // 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,16 +101,18 @@
     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 final long mDataVersion[] = new long[2 * SCREEN_NAIL_MAX + 1];
+    private final int mFromIndex[] = new int[2 * SCREEN_NAIL_MAX + 1];
+
+    private final GestureRecognizer mGestureRecognizer;
 
     private PhotoTapListener mPhotoTapListener;
 
@@ -95,8 +121,7 @@
     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;
 
@@ -107,15 +132,28 @@
 
     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;
+
+    // [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.
+    // HOLD_TOUCH_DOWN_FROM_CAMERA is an extra flag set together with
+    // HOLD_TOUCH_DOWN if the touch down starts from camera preview.
+    private int mHolding;
+    private static final int HOLD_TOUCH_DOWN = 1;
+    private static final int HOLD_TOUCH_DOWN_FROM_CAMERA = 2;
+    private static final int HOLD_CAPTURE_ANIMATION = 4;
 
     public PhotoView(GalleryActivity activity) {
-        mActivity = activity;
         mTileView = new TileImageView(activity);
         addComponent(mTileView);
         Context context = activity.getAndroidContext();
@@ -129,154 +167,88 @@
                 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;
-                    }
-                    case MSG_SHOW_LOADING: {
-                        if (mLoadingState == LOADING_INIT) {
-                            // We don't need the opening animation
-                            mOpenedItemPath = null;
+        mHandler = new MyHandler(activity.getGLRoot());
 
-                            mLoadingSpinner.startAnimation();
-                            mLoadingState = LOADING_TIMEOUT;
-                            invalidate();
+        mGestureRecognizer = new GestureRecognizer(
+                context, new MyGestureListener());
+
+        mPositionController = new PositionController(context,
+                new PositionController.Listener() {
+                    public void invalidate() {
+                        PhotoView.this.invalidate();
+                    }
+                    public boolean isHolding() {
+                        // We want the film mode change happen as soon as
+                        // possible even if the touch is still down.
+                        if ((mHolding & HOLD_TOUCH_DOWN_FROM_CAMERA) != 0) {
+                            return false;
+                        } else {
+                            return mHolding != 0;
                         }
-                        break;
                     }
-                    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);
+        Arrays.fill(mDataVersion, INVALID_DATA_VERSION);
+        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));
-        }
-    }
+        @Override
+        public void handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_SHOW_LOADING: {
+                    if (mLoadingState == LOADING_INIT) {
+                        // We don't need the opening animation
+                        mPositionController.setOpenAnimationRect(null);
 
-    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);
+                        mLoadingSpinner.startAnimation();
+                        mLoadingState = LOADING_TIMEOUT;
+                        invalidate();
+                    }
+                    break;
                 }
-                updateLoadingState();
-                break;
+                case MSG_CANCEL_EXTRA_SCALING: {
+                    mGestureRecognizer.cancelScale();
+                    mPositionController.setExtraScalingRange(false);
+                    mCancelExtraScalingPending = false;
+                    break;
+                }
+                case MSG_SWITCH_FOCUS: {
+                    switchFocus();
+                    break;
+                }
+                case MSG_CAPTURE_ANIMATION_DONE: {
+                    captureAnimationDone();
+                    break;
+                }
+                default: throw new AssertionError(message.what);
             }
         }
-    }
+    };
 
     private void updateLoadingState() {
         // Possible transitions of mLoadingState:
@@ -284,12 +256,14 @@
         //     TIMEOUT --> COMPLETE, FAIL, INIT
         //    COMPLETE --> INIT
         //        FAIL --> INIT
-        if (mModel.getLevelCount() != 0 || mModel.getBackupImage() != null) {
+        if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) {
             mHandler.removeMessages(MSG_SHOW_LOADING);
             mLoadingState = LOADING_COMPLETE;
         } else if (mModel.isFailedToLoad()) {
             mHandler.removeMessages(MSG_SHOW_LOADING);
             mLoadingState = LOADING_FAIL;
+            // We don't want the opening animation after loading failure
+            mPositionController.setOpenAnimationRect(null);
         } else if (mLoadingState != LOADING_INIT) {
             mLoadingState = LOADING_INIT;
             mHandler.removeMessages(MSG_SHOW_LOADING);
@@ -298,534 +272,281 @@
         }
     }
 
-    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();
+    ////////////////////////////////////////////////////////////////////////////
+    //  Data/Image change notifications
+    ////////////////////////////////////////////////////////////////////////////
 
-        if (mModel == null) {
-            mTileView.notifyModelInvalidated();
-            mTileView.setAlpha(1.0f);
-            mImageRotation = 0;
-            mPositionController.setImageSize(0, 0);
-            updateLoadingState();
-        } else {
-            notifyImageInvalidated(0);
-        }
-    }
+    public void notifyDataChange(long[] versions, int prevBound, int nextBound) {
+        mPrevBound = prevBound;
+        mNextBound = nextBound;
 
-    @Override
-    protected boolean onTouch(MotionEvent event) {
-        mGestureDetector.onTouchEvent(event);
-        mScaleDetector.onTouchEvent(event);
-        mDownUpDetector.onTouchEvent(event);
-        return true;
-    }
-
-    @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);
-        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.
-        int w = getWidth();
-        int h = getHeight();
-        int s = Math.min(getWidth(), getHeight()) / 6;
-
-        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);
-        }
-
-        // 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);
-        }
-    }
-
-    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();
-        }
-    }
-
-    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;
-    }
-
-    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 boolean mIgnoreUpEvent = false;
-
-    private class MyGestureListener
-            extends GestureDetector.SimpleOnGestureListener {
-        @Override
-        public boolean onScroll(
-                MotionEvent e1, MotionEvent e2, float dx, float dy) {
-            if (mTransitionMode != TRANS_NONE) return true;
-
-            ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
-            ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
-
-            mPositionController.startScroll(dx, dy, next.isEnabled(),
-                    prev.isEnabled());
-            return true;
-        }
-
-        @Override
-        public boolean onSingleTapUp(MotionEvent e) {
-            if (mPhotoTapListener != null) {
-                mPhotoTapListener.onSingleTapUp((int) e.getX(), (int) e.getY());
-            }
-            return true;
-        }
-
-        @Override
-        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
-                float velocityY) {
-            if (swipeImages(velocityX)) {
-                mIgnoreUpEvent = true;
-            } else if (mTransitionMode != TRANS_NONE) {
-                // do nothing
-            } else if (mPositionController.fling(velocityX, velocityY)) {
-                mIgnoreUpEvent = true;
-            }
-            return true;
-        }
-
-        @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();
-            }
-            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;
-                }
-            } else {
-                if (mCancelExtraScalingPending) {
-                    mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
-                    mPositionController.setExtraScalingRange(false);
-                    mCancelExtraScalingPending = false;
-                }
-            }
-            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) {
-            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);
+        // Check if the data version actually changed.
+        boolean changed = false;
+        int N = 2 * SCREEN_NAIL_MAX + 1;
+        for (int i = 0; i < N; i++) {
+            if (versions[i] != mDataVersion[i]) {
+                changed = true;
                 break;
             }
-            default: throw new IllegalArgumentException(String.valueOf(direction));
+        }
+        if (!changed) return;
+
+        // Create the mFromIndex array, which records the index where the picture
+        // come from. The value Integer.MAX_VALUE means it's a new picture.
+        for (int i = 0; i < N; i++) {
+            long v = versions[i];
+            if (v == INVALID_DATA_VERSION) {
+                mFromIndex[i] = Integer.MAX_VALUE;
+                continue;
+            }
+
+            // Try to find the same version number in the old array
+            int j;
+            for (j = 0; j < N; j++) {
+                if (mDataVersion[j] == v) {
+                    break;
+                }
+            }
+            mFromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
+        }
+
+        // Copy the new data version
+        for (int i = 0; i < N; i++) {
+            mDataVersion[i] = versions[i];
+        }
+
+        // Move the boxes
+        mPositionController.moveBox(mFromIndex, mPrevBound < 0, mNextBound > 0);
+
+        // Update the ScreenNails.
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            mPictures.get(i).reload();
+        }
+
+        invalidate();
+    }
+
+    public void notifyImageChange(int index) {
+        mPictures.get(index).reload();
+        invalidate();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Pictures
+    ////////////////////////////////////////////////////////////////////////////
+
+    private interface Picture {
+        void reload();
+        void draw(GLCanvas canvas, Rect r);
+        void setScreenNail(ScreenNail s);
+        boolean isCamera();  // whether the picture is a camera preview
+    };
+
+    private boolean isCameraScreenNail(ScreenNail s) {
+        return s != null && !(s instanceof BitmapScreenNail);
+    }
+
+    class FullPicture implements Picture {
+        private int mRotation;
+        private boolean mIsCamera;
+        private boolean mWasCenter;
+
+        public void FullPicture(TileImageView tileView) {
+            mTileView = tileView;
+        }
+
+        @Override
+        public void reload() {
+            // mImageWidth and mImageHeight will get updated
+            mTileView.notifyModelInvalidated();
+            mTileView.setAlpha(1.0f);
+
+            mRotation = mModel.getImageRotation(0);
+            int w = mTileView.mImageWidth;
+            int h = mTileView.mImageHeight;
+            mPositionController.setImageSize(0,
+                    getRotated(mRotation, w, h),
+                    getRotated(mRotation, h, w));
+
+            setScreenNail(mModel.getScreenNail(0));
+            updateLoadingState();
+        }
+
+        @Override
+        public void draw(GLCanvas canvas, Rect r) {
+            if (mLoadingState == LOADING_COMPLETE) {
+                setTileViewPosition(r);
+                PhotoView.super.render(canvas);
+            }
+            renderMessage(canvas, r.centerX(), r.centerY());
+
+            boolean isCenter = r.centerX() == getWidth() / 2;
+
+            // We want to have 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.
+            if ((mHolding & ~(HOLD_TOUCH_DOWN | HOLD_TOUCH_DOWN_FROM_CAMERA)) == 0) {
+                if (mWasCenter && !isCenter && mIsCamera && !mFilmMode) {
+                    setFilmMode(true);
+                } else if (mIsCamera && isCenter && mFilmMode) {
+                    setFilmMode(false);
+                }
+            }
+            mWasCenter = isCenter;
+        }
+
+        @Override
+        public void setScreenNail(ScreenNail s) {
+            mIsCamera = isCameraScreenNail(s);
+            mTileView.setScreenNail(s);
+        }
+
+        @Override
+        public boolean isCamera() {
+            return mIsCamera;
+        }
+
+        private void setTileViewPosition(Rect r) {
+            TileImageView t = mTileView;
+
+            // Find out the bitmap coordinates of the center of the view
+            int imageW = mPositionController.getImageWidth();
+            int imageH = mPositionController.getImageHeight();
+            float scale = mPositionController.getImageScale();
+            int viewW = getWidth();
+            int viewH = getHeight();
+            int centerX = (int) (imageW / 2f +
+                    (viewW / 2f - r.exactCenterX()) / scale + 0.5f);
+            int centerY = (int) (imageH / 2f +
+                    (viewH / 2f - r.exactCenterY()) / scale + 0.5f);
+
+            boolean wantsCardEffect = CARD_EFFECT && !mFilmMode
+                && !mPictures.get(-1).isCamera();
+            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) {
+                    if (right - left < viewW) {
+                        // If the picture is narrower than the view, keep it at
+                        // the center of the view.
+                        centerX = imageW / 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(viewW / 2f / scale);
+                    }
+                    scale *= getScrollScale(progress);
+                    t.setAlpha(getScrollAlpha(progress));
+                }
+            }
+
+            // set the position of the tile view
+            int inverseX = imageW - centerX;
+            int inverseY = imageH - centerY;
+            int rotation = mRotation;
+            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));
+            }
+        }
+
+        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.
+            int w = getWidth();
+            int h = getHeight();
+            int s = Math.min(getWidth(), getHeight()) / 6;
+
+            if (mLoadingState == LOADING_TIMEOUT) {
+                StringTexture m = mLoadingText;
+                ProgressSpinner p = mLoadingSpinner;
+                p.draw(canvas, x - p.getWidth() / 2, y - p.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);
+            }
+
+            // Draw a debug indicator showing which picture has focus (index ==
+            // 0).
+            // canvas.fillRect(x - 10, y - 10, 20, 20, 0x80FF00FF);
+
+            // 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);
+            }
         }
     }
 
-    private class MyDownUpListener implements DownUpDetector.DownUpListener {
-        public void onDown(MotionEvent e) {
+    private class ScreenNailPicture implements Picture {
+        private int mIndex;
+        private int mRotation;
+        private ScreenNail mScreenNail;
+        private Size mSize = new Size();
+        private boolean mIsCamera;
+
+        public ScreenNailPicture(int index) {
+            mIndex = index;
         }
 
-        public void onUp(MotionEvent e) {
-            mEdgeView.onRelease();
+        @Override
+        public void reload() {
+            setScreenNail(mModel.getScreenNail(mIndex));
+        }
 
-            if (mIgnoreUpEvent) {
-                mIgnoreUpEvent = false;
+        @Override
+        public void draw(GLCanvas canvas, Rect r) {
+            if (mScreenNail == null) {
+                // Draw a placeholder rectange if there will be a picture in
+                // this position.
+                if (mIndex >= mPrevBound && mIndex <= mNextBound) {
+                    canvas.fillRect(r.left, r.top, r.width(), r.height(),
+                            PLACEHOLDER_COLOR);
+                }
                 return;
             }
-            if (!snapToNeighborImage() && mTransitionMode == TRANS_NONE) {
-                mPositionController.up();
+            if (r.left >= getWidth() || r.right <= 0 ||
+                    r.top >= getHeight() || r.bottom <= 0) {
+                mScreenNail.noDraw();
+                return;
             }
-        }
-    }
 
-    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();
-    }
-
-    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();
-    }
-
-    public void notifyTransitionComplete() {
-        mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE);
-    }
-
-    private void onTransitionComplete() {
-        int mode = mTransitionMode;
-        mTransitionMode = TRANS_NONE;
-
-        if (mModel == null) return;
-        if (mode == TRANS_SWITCH_NEXT) {
-            switchToNextImage();
-        } else if (mode == TRANS_SWITCH_PREVIOUS) {
-            switchToPreviousImage();
-        }
-    }
-
-    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();
-            }
-        }
-
-        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;
+            boolean wantsCardEffect = CARD_EFFECT && !mFilmMode
+                && (mIndex > 0) && !mPictures.get(0).isCamera();
 
             int w = getWidth();
-            int x = applyFadingAnimation ? w / 2 : mOffsetX;
-            int y = getHeight() / 2;
+            int drawW = getRotated(mRotation, r.width(), r.height());
+            int drawH = getRotated(mRotation, r.height(), r.width());
+            int cx = wantsCardEffect ? w / 2 : r.centerX();
+            int cy = r.centerY();
             int flags = GLCanvas.SAVE_FLAG_MATRIX;
 
-            if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA;
+            if (wantsCardEffect) flags |= GLCanvas.SAVE_FLAG_ALPHA;
             canvas.save(flags);
-            canvas.translate(x, y, 0);
-            if (applyFadingAnimation) {
-                float progress = (float) (x - mOffsetX) / w;
+            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);
                 canvas.multiplyAlpha(alpha);
@@ -834,13 +555,436 @@
             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);
+            mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
             canvas.restore();
         }
+
+        @Override
+        public void setScreenNail(ScreenNail s) {
+            if (mScreenNail == s) return;
+            mScreenNail = s;
+            mIsCamera = isCameraScreenNail(s);
+            mRotation = mModel.getImageRotation(mIndex);
+
+            int w = 0, h = 0;
+            if (mScreenNail != null) {
+                w = s.getWidth();
+                h = s.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;
+            }
+
+            if (w != 0 && h != 0)  {
+                mPositionController.setImageSize(mIndex,
+                        getRotated(mRotation, w, h),
+                        getRotated(mRotation, h, w));
+            }
+        }
+
+        @Override
+        public boolean isCamera() {
+            return mIsCamera;
+        }
     }
 
+    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 mode in this scaling gesture.
+        private boolean mModeChanged;
+
+        @Override
+        public boolean onSingleTapUp(float x, float y) {
+            if (mFilmMode) {
+                setFilmMode(false);
+                return true;
+            }
+
+            if (mPhotoTapListener != null) {
+                mPhotoTapListener.onSingleTapUp((int) x, (int) y);
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onDoubleTap(float x, float y) {
+            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) {
+            mPositionController.startScroll(-dx, -dy);
+            return true;
+        }
+
+        @Override
+        public boolean onFling(float velocityX, float velocityY) {
+            if (swipeImages(velocityX, velocityY)) {
+                mIgnoreUpEvent = true;
+            } else if (mPositionController.fling(velocityX, velocityY)) {
+                mIgnoreUpEvent = true;
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onScaleBegin(float focusX, float focusY) {
+            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;
+        }
+
+        @Override
+        public boolean onScale(float focusX, float focusY, float scale) {
+            if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
+            int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
+
+            // We allow only one mode change in a scaling gesture.
+            if (mCanChangeMode && !mModeChanged) {
+                if ((outOfRange < 0 && !mFilmMode) ||
+                        (outOfRange > 0 && mFilmMode)) {
+                    setFilmMode(!mFilmMode);
+                    mModeChanged = true;
+                    return true;
+                }
+           }
+
+            if (outOfRange != 0 && !mModeChanged) {
+                startExtraScalingIfNeeded();
+            } else {
+                stopExtraScalingIfNeeded();
+            }
+            return true;
+        }
+
+        private void startExtraScalingIfNeeded() {
+            if (!mCancelExtraScalingPending) {
+                mHandler.sendEmptyMessageDelayed(
+                        MSG_CANCEL_EXTRA_SCALING, 700);
+                mPositionController.setExtraScalingRange(true);
+                mCancelExtraScalingPending = true;
+            }
+        }
+
+        private void stopExtraScalingIfNeeded() {
+            if (mCancelExtraScalingPending) {
+                mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
+                mPositionController.setExtraScalingRange(false);
+                mCancelExtraScalingPending = false;
+            }
+        }
+
+        @Override
+        public void onScaleEnd() {
+            mPositionController.endScale();
+        }
+
+        @Override
+        public void onDown() {
+            mHolding |= HOLD_TOUCH_DOWN;
+            if (mPictures.get(0).isCamera()) {
+                mHolding |= HOLD_TOUCH_DOWN_FROM_CAMERA;
+            }
+        }
+
+        @Override
+        public void onUp() {
+            mHolding &= ~(HOLD_TOUCH_DOWN | HOLD_TOUCH_DOWN_FROM_CAMERA);
+            mEdgeView.onRelease();
+
+            if (mIgnoreUpEvent) {
+                mIgnoreUpEvent = false;
+                return;
+            }
+
+            snapback();
+        }
+    }
+
+    private void setFilmMode(boolean enabled) {
+        if (mFilmMode == enabled) return;
+        mFilmMode = enabled;
+        mPositionController.setFilmMode(mFilmMode);
+        mModel.setNeedFullImage(!enabled);
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Framework events
+    ////////////////////////////////////////////////////////////////////////////
+
+    @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);
+        if (changeSize) {
+            mPositionController.setViewSize(getWidth(), getHeight());
+        }
+    }
+
+    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();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Rendering
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void render(GLCanvas canvas) {
+        // Draw next photos
+        for (int i = 1; i <= SCREEN_NAIL_MAX; i++) {
+            Rect r = mPositionController.getPosition(i);
+            mPictures.get(i).draw(canvas, r);
+            // In page mode, we draw only one next photo.
+            if (!mFilmMode) break;
+        }
+
+        // Draw current photo
+        mPictures.get(0).draw(canvas, mPositionController.getPosition(0));
+
+        // Draw previous photos
+        for (int i = -1; i >= -SCREEN_NAIL_MAX; i--) {
+            Rect r = mPositionController.getPosition(i);
+            mPictures.get(i).draw(canvas, r);
+            // In page mode, we draw only one previous photo.
+            if (!mFilmMode) break;
+        }
+
+        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;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  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() {
+        mModel.next();
+    }
+
+    private void switchToPrevImage() {
+        mModel.previous();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Opening Animation
+    ////////////////////////////////////////////////////////////////////////////
+
+    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 (mFilmMode) return false;
+        if (mHolding != 0) return true;
+        if (offset == 1) {
+            if (mNextBound <= 0) return false;
+            switchToNextImage();
+            mPositionController.startCaptureAnimationSlide(-1);
+        } else if (offset == -1) {
+            if (mPrevBound >= 0) return false;
+            switchToPrevImage();
+            mPositionController.startCaptureAnimationSlide(1);
+        } else {
+            return false;
+        }
+        mHolding |= HOLD_CAPTURE_ANIMATION;
+        mHandler.sendEmptyMessageDelayed(MSG_CAPTURE_ANIMATION_DONE, 800);
+        return true;
+    }
+
+    private void captureAnimationDone() {
+        mHolding &= ~HOLD_CAPTURE_ANIMATION;
+        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
@@ -917,44 +1061,15 @@
         }
     }
 
-    public void pause() {
-        mPositionController.skipAnimation();
-        mTransitionMode = TRANS_NONE;
-        mTileView.freeTextures();
-        for (ScreenNailEntry entry : mScreenNails) {
-            entry.set(false, null, 0);
-        }
-    }
+    ////////////////////////////////////////////////////////////////////////////
+    //  Simple public utilities
+    ////////////////////////////////////////////////////////////////////////////
 
-    public void resume() {
-        mTileView.prepareTextures();
-    }
-
-    public void setOpenedItem(Path itemPath) {
-        mOpenedItemPath = itemPath;
+    public void setPhotoTapListener(PhotoTapListener listener) {
+        mPhotoTapListener = listener;
     }
 
     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;
-    }
 }
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
index 2068446..e6c132b 100644
--- a/src/com/android/gallery3d/ui/PositionController.java
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -16,353 +16,449 @@
 
 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;
+
+    // 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
+        600,  // ANIM_KIND_OPENING
         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
+        800,  // 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;
+    private int mViewW = 640;
+    private int mViewH = 480;;
+
+    // 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;
+    //
+    //  ___________________________________________________________
+    // |   _____       _____       _____       _____       _____   |
+    // |  |     |     |     |     |     |     |     |     |     |  |
+    // |  | Box |     | Box |     | Box*|     | Box |     | Box |  |
+    // |  |_____|.....|_____|.....|_____|.....|_____|.....|_____|  |
+    // |          Gap         Gap         Gap         Gap          |
+    // |___________________________________________________________|
+    //
+    //                       <--  Platform  -->
+    //
+    // The focused box (Box*) centers at mPlatform.mCurrentX
 
-    private RectF mTempRect = new RectF();
-    private float[] mTempPoints = new float[8];
+    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);
 
-    public PositionController(PhotoView viewer, Context context,
-            EdgeView edgeView) {
-        mViewer = viewer;
-        mEdgeView = edgeView;
-        mScroller = new FlingScroller();
+    // 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);
 
-        // If no image available, use view size.
+        // Initialize the areas.
+        initPlatform();
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            mBoxes.put(i, new Box());
+            initBox(i);
+            mRects.put(i, new Rect());
+        }
+        for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+            mGaps.put(i, new Gap());
+            initGap(i);
+        }
+    }
+
+    public void setOpenAnimationRect(Rect r) {
+        mOpenAnimationRect = r;
+    }
+
+    public void setViewSize(int viewW, int viewH) {
+        if (viewW == mViewW && viewH == mViewH) return;
+
+        mViewW = viewW;
+        mViewH = viewH;
+        initPlatform();
+
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            setBoxSize(i, viewW, viewH, true);
+        }
+
+        updateScaleAndGapLimit();
+        snapAndRedraw();
+    }
+
+    public void setImageSize(int index, int width, int height) {
         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);
+            initBox(index);
+        } else if (!setBoxSize(index, width, height, false)) {
             return;
         }
 
-        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);
-        }
-        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+        updateScaleAndGapLimit();
+        startOpeningAnimationIfNeeded();
+        snapAndRedraw();
     }
 
-    public void zoomIn(float tapX, float tapY, float targetScale) {
-        if (targetScale > mScaleMax) targetScale = mScaleMax;
+    // 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;
 
-        // 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);
+        // 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.
+        float ratio = Math.min(
+                (float) b.mImageW / width, (float) 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(width, height);
+            b.mAnimationStartTime = NO_ANIMATION;
+        } else {
+            b.mCurrentScale *= ratio;
+            b.mFromScale *= ratio;
+            b.mToScale *= ratio;
+        }
+
+        b.mImageW = width;
+        b.mImageH = height;
+
+        if (i == 0) {
+            mFocusX /= ratio;
+            mFocusY /= ratio;
+        }
+
+        return true;
+    }
+
+    private void startOpeningAnimationIfNeeded() {
+        if (mOpenAnimationRect == null) return;
+        Box b = mBoxes.get(0);
+        if (b.mUseViewSize) return;
+
+        // Start animation from the saved rectangle if we have one.
+        Rect r = mOpenAnimationRect;
+        mOpenAnimationRect = null;
+        mPlatform.mCurrentX = r.centerX();
+        b.mCurrentY = r.centerY();
+        b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
+                r.height() / (float) b.mImageH);
+        startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_OPENING);
+    }
+
+    public void setFilmMode(boolean enabled) {
+        if (enabled == mFilmMode) return;
+        mFilmMode = enabled;
+
+        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, and mode.
+    private void updateScaleAndGapLimit() {
+        for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+            Box b = mBoxes.get(i);
+            b.mScaleMin = getMinimalScale(b.mImageW, b.mImageH);
+            b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
+        }
+
+        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.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();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Start an animations for the focused box
+    ////////////////////////////////////////////////////////////////////////////
+
+    public void zoomIn(float tapX, float tapY, float targetScale) {
+        Box b = mBoxes.get(0);
+
+        // Convert the tap position to distance to center in bitmap coordinates
+        float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
+        float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
+
+        int x = (int) (mViewW / 2 - tempX * targetScale + 0.5f);
+        int y = (int) (mViewH / 2 - 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(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_ZOOM);
     }
 
     public void beginScale(float focusX, float focusY) {
+        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) {
+        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 *= 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(mViewW / 2, mViewH / 2, 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(mViewW / 2, ANIM_KIND_CAPTURE);
+        b.doAnimation(mViewH / 2, b.mScaleMin, ANIM_KIND_CAPTURE);
+        n.doAnimation(mViewH / 2, 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;
+
+        int x = getTargetX(p) + (int) (dx + 0.5f);
+        int y = getTargetY(b) + (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,213 +466,517 @@
 
         // 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.
+        int cx = mViewW / 2;
+        if (!mHasPrev && x > cx) {
+            int pixels = x - cx;
+            mListener.onPull(pixels, EdgeView.LEFT);
+            x = cx;
+        } else if (!mHasNext && x < cx) {
+            int pixels = cx - x;
+            mListener.onPull(pixels, EdgeView.RIGHT);
+            x = cx;
+        }
+
+        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 cx = mViewW / 2;
+        if ((!mHasPrev && p.mCurrentX >= cx)
+                || (!mHasNext && p.mCurrentX <= cx)) {
             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();
+        }
+        redraw();
+    }
+
+    private void startAnimation(int targetX, int targetY, float targetScale,
+            int kind) {
+        boolean changed = false;
+        changed |= mPlatform.doAnimation(targetX, 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();
+        }
+        if (changed) redraw();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  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;
+        int w = widthOf(b);
+        int h = heightOf(b);
+        if (i == 0) {
+            int x = mPlatform.mCurrentX;
+            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;
+    // Returns the position of a box.
+    public Rect getPosition(int index) {
+        return mRects.get(index);
+    }
 
-        float scaleMin = mExtraScalingRange ?
-                mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
-        float scaleMax = mExtraScalingRange ?
-                mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
+    ////////////////////////////////////////////////////////////////////////////
+    //  Box management
+    ////////////////////////////////////////////////////////////////////////////
 
-        if (mCurrentScale < scaleMin || mCurrentScale > scaleMax) {
-            needAnimation = true;
-            scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
+    // Initialize the platform to be at the view center.
+    private void initPlatform() {
+        mPlatform.mCurrentX = mViewW / 2;
+        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.mImageW, b.mImageH);
+        b.mScaleMax = getMaximalScale(b.mImageW, b.mImageH);
+        b.mCurrentY = mViewH / 2;
+        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.
+    public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext) {
+        //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();
         }
 
-        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;
+        // 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);
         }
 
-        if (needAnimation) {
-            startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
+        // 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);
         }
 
-        return needAnimation;
+        // 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. offset the Platform position
+        int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
+        mPlatform.mCurrentX += dx;
+        mPlatform.mFromX += dx;
+        mPlatform.mToX += dx;
+        mPlatform.mFlingOffset += dx;
+
+        snapAndRedraw();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Public utilities
+    ////////////////////////////////////////////////////////////////////////////
+
+    public float getMinimalScale(int imageW, int imageH) {
+        float wFactor = 1.0f;
+        float hFactor = 1.0f;
+
+        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 * mViewW / imageW,
+                hFactor * mViewH / imageH);
+        return Math.min(SCALE_LIMIT, s);
+    }
+
+    public float getMaximalScale(int imageW, int imageH) {
+        return mFilmMode ? getMinimalScale(imageW, imageH) : SCALE_LIMIT;
+    }
+
+    public boolean isAtMinimalScale() {
+        Box b = mBoxes.get(0);
+        return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
+    }
+
+    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;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Private utilities
+    ////////////////////////////////////////////////////////////////////////////
+
+    private float getMinimalScale(Box b) {
+        return getMinimalScale(b.mImageW, b.mImageH);
+    }
+
+    private float getMaxmimalScale(Box b) {
+        return getMaximalScale(b.mImageW, b.mImageH);
+    }
+
+    private static boolean isAlmostEqual(float a, float b) {
+        float diff = a - b;
+        return (diff < 0 ? -diff : diff) < 0.02f;
     }
 
     // Calculates the stable region of mCurrent{X/Y}, where "stable" means
@@ -594,95 +994,439 @@
     // 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 - horizontalSlack) - w / 2;
+        mBoundRight = mViewW - mBoundLeft;
+        mBoundTop = mViewH - h / 2;
+        mBoundBottom = mViewH - mBoundTop;
 
         // 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 = mViewH / 2;
         }
 
         // Same for width
-        if (Math.floor(mImageW * scale) <= mViewW) {
-            mBoundLeft = mBoundRight = mImageW / 2;
+        if (viewWiderThanScaledImage(scale)) {
+            mBoundLeft = mBoundRight = mViewW / 2;
         }
     }
 
-    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 useCurrentValueAsTarget(b) ? b.mCurrentScale : b.mToScale;
     }
 
-    public RectF getImageBounds() {
-        float points[] = mTempPoints;
+    private int getTargetX(Platform p) {
+        return useCurrentValueAsTarget(p) ? p.mCurrentX : p.mToX;
+    }
 
-        /*
-         * (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;
+    private int getTargetY(Box b) {
+        return useCurrentValueAsTarget(b) ? b.mCurrentY : b.mToY;
+    }
 
-        RectF rect = mTempRect;
-        rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
-                Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+    private boolean useCurrentValueAsTarget(Animatable a) {
+        return a.mAnimationStartTime == NO_ANIMATION ||
+                a.mAnimationKind == ANIM_KIND_SNAPBACK ||
+                a.mAnimationKind == ANIM_KIND_FLING;
+    }
 
-        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;
+    // Returns the index of the anchor box.
+    private int anchorIndex(int i) {
+        if (i > 0) return i - 1;
+        if (i < 0) return i + 1;
+        throw new IllegalArgumentException();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    //  Animatable: an thing which can do animation.
+    ////////////////////////////////////////////////////////////////////////////
+    private abstract static class Animatable {
+        public long mAnimationStartTime;
+        public int mAnimationKind;
+        public int mAnimationDuration;
+
+        // 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();
+
+        // Returns true if the animation values changes, so things need to be
+        // redrawn.
+        public boolean advanceAnimation() {
+            if (mAnimationStartTime == NO_ANIMATION) {
+                return false;
+            }
+            if (mAnimationStartTime == LAST_ANIMATION) {
+                mAnimationStartTime = NO_ANIMATION;
+                return startSnapback();
+            }
+
+            float progress;
+            if (mAnimationDuration == 0) {
+                progress = 1;
+            } else {
+                long now = AnimationTime.get();
+                progress =
+                    (float) (now - mAnimationStartTime) / mAnimationDuration;
+            }
+
+            if (progress >= 1) {
+                progress = 1;
+            } else {
+                progress = applyInterpolationCurve(mAnimationKind, progress);
+            }
+
+            boolean done = interpolate(progress);
+
+            if (done) {
+                mAnimationStartTime = LAST_ANIMATION;
+            }
+
+            return true;
         }
-        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_SNAPBACK:
+                case ANIM_KIND_ZOOM:
+                case ANIM_KIND_SLIDE:
+                case ANIM_KIND_OPENING:
+                    progress = 1 - f * f * f * f * f; // x^5
+                    break;
+            }
+            return progress;
+        }
     }
 
-    public int getImageWidth() {
-        return mImageW;
+    ////////////////////////////////////////////////////////////////////////////
+    //  Platform: captures the global X movement.
+    ////////////////////////////////////////////////////////////////////////////
+    private class Platform extends Animatable {
+        public int mCurrentX, mFromX, mToX;
+        public int mFlingOffset;
+
+        @Override
+        public boolean startSnapback() {
+            if (mAnimationStartTime != NO_ANIMATION) return false;
+            if (mAnimationKind == ANIM_KIND_SCROLL
+                    && mListener.isHolding()) 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;
+            if (mFilmMode) {
+                if (!mHasNext) x = Math.max(x, mViewW / 2);
+                if (!mHasPrev) x = Math.min(x, mViewW / 2);
+            } else {
+                calculateStableBound(scale, HORIZONTAL_SLACK);
+                x = Utils.clamp(x, mBoundLeft, mBoundRight);
+            }
+            if (mCurrentX != x) {
+                return doAnimation(x, ANIM_KIND_SNAPBACK);
+            }
+            return false;
+        }
+
+        // Starts an animation for the platform.
+        public boolean doAnimation(int targetX, int kind) {
+            if (mCurrentX == targetX) return false;
+            mAnimationKind = kind;
+            mFromX = mCurrentX;
+            mToX = targetX;
+            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 < mViewW / 2) {
+                if (!mHasNext) {
+                    dir = EdgeView.RIGHT;
+                }
+            } else if (mCurrentX > mViewW / 2) {
+                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 = mViewW / 2;
+            }
+            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;
+                return true;
+            } else {
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    progress = CaptureAnimation.calculateSlide(progress);
+                    mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+                    return false;
+                } else {
+                    mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+                    return (mCurrentX == mToX);
+                }
+            }
+        }
     }
 
-    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;
+            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 = mViewH / 2;
+                } else {
+                    calculateStableBound(scale, HORIZONTAL_SLACK);
+                    y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
+                }
+            } else {
+                y = mViewH / 2;
+                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 = Utils.clamp(targetScale,
+                    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 this for height only, not width, because the
+            // user may want to scroll to the previous/next image.)
+            if (!mInScale && viewTallerThanScaledImage(targetScale)) {
+                targetY = mViewH / 2;
+            }
+
+            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;
+        }
+
+        @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;
 
-    public boolean isAtRightEdge() {
-        calculateStableBound(mCurrentScale);
-        return mCurrentX >= mBoundRight;
+        // 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);
+                }
+            }
+        }
     }
 }
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/Raw2DTexture.java b/src/com/android/gallery3d/ui/Raw2DTexture.java
new file mode 100644
index 0000000..c23bf94
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Raw2DTexture.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.opengl.GLU;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+/**
+ * A wrapper class of GL 2D texture.
+ */
+public class Raw2DTexture extends BasicTexture {
+    private static final int[] sTextureId = new int[1];
+    // OpenGL related fields used when copy.
+    private static final int[] sBufferName = new int[1];
+    private int mFBO;  // Frame buffer object.
+
+    // We need copy from another texture when in camera capture animation.
+    // The copy is done through a framebuffer object with an attached
+    // destination texture. The source texture is rendered to the framebuffer
+    // and the data will be stored in the destination texture.
+    public static void copy(GLCanvas canvas, BasicTexture src, Raw2DTexture dst) {
+        int[] viewPort = new int[4];
+        GL11 gl11 = canvas.getGLInstance();
+        GL11ExtensionPack gl11ep = (GL11ExtensionPack) gl11;
+
+        if (!dst.isLoaded(canvas)) {
+            dst.prepare(canvas.getGLInstance());
+        }
+
+        gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, dst.mFBO);
+        gl11ep.glFramebufferTexture2DOES(
+                GL11ExtensionPack.GL_FRAMEBUFFER_OES,
+                GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES,
+                GL11.GL_TEXTURE_2D,
+                dst.getId(), 0);
+        checkFramebufferStatus(gl11ep);
+
+        // Draw the source onto our destination.
+        // The texture coords and vertex pointer are already set properly. We don't
+        // need to set again.
+        gl11.glBindTexture(src.getTarget(), src.getId());
+        boolean targetEnabled = gl11.glIsEnabled(src.getTarget());
+        gl11.glEnable(src.getTarget());
+        boolean scissorEnabled = gl11.glIsEnabled(GL11.GL_SCISSOR_TEST);
+        gl11.glDisable(GL11.GL_SCISSOR_TEST);
+
+        // Set the texture matrix.
+        gl11.glMatrixMode(GL11.GL_TEXTURE);
+        gl11.glPushMatrix();
+        gl11.glLoadIdentity();
+
+        // Set the view port.
+        gl11.glGetIntegerv(GL11.GL_VIEWPORT, viewPort, 0);
+        gl11.glViewport(0, 0, dst.getTextureWidth(), dst.getTextureHeight());
+
+        // Set the projection matrix.
+        gl11.glMatrixMode(GL11.GL_PROJECTION);
+        gl11.glPushMatrix();
+        gl11.glLoadIdentity();
+        GLU.gluOrtho2D(gl11, 0, 1, 0, 1);
+
+        // Set the modelview matrix.
+        gl11.glMatrixMode(GL11.GL_MODELVIEW);
+        gl11.glPushMatrix();
+        gl11.glLoadIdentity();
+
+        // Draw the texture.
+        gl11.glDrawArrays(GL11.GL_TRIANGLE_STRIP, 0, 4);
+
+        // Clear.
+        if (!targetEnabled) gl11.glDisable(src.getTarget());
+        if (scissorEnabled) gl11.glEnable(GL11.GL_SCISSOR_TEST);
+        gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0);
+        gl11.glBindTexture(src.getTarget(), 0);
+        gl11.glViewport(viewPort[0], viewPort[1], viewPort[2], viewPort[3]);
+        gl11.glMatrixMode(GL11.GL_TEXTURE);
+        gl11.glPopMatrix();
+        gl11.glMatrixMode(GL11.GL_PROJECTION);
+        gl11.glPopMatrix();
+        gl11.glMatrixMode(GL11.GL_MODELVIEW);
+        gl11.glPopMatrix();
+        GLId.glDeleteFramebuffers(gl11ep, 1, dst.sBufferName, 0);
+    }
+
+    public Raw2DTexture(int w, int h) {
+        setSize(w, h);
+        GLId.glGenTextures(1, sTextureId, 0);
+        mId = sTextureId[0];
+        GLId.glGenBuffers(1, sBufferName, 0);
+        mFBO = sBufferName[0];
+    }
+
+    private void prepare(GL11 gl11) {
+        gl11.glBindTexture(GL11.GL_TEXTURE_2D, mId);
+        gl11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+        gl11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+        gl11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+        gl11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+        gl11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA,
+                getTextureWidth(), getTextureHeight(),
+                0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, null);
+        gl11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
+        mState = UploadedTexture.STATE_LOADED;
+    }
+
+    @Override
+    protected boolean onBind(GLCanvas canvas) {
+        if (!isLoaded(canvas)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int getTarget() {
+        return GL11.GL_TEXTURE_2D;
+    }
+
+    public boolean isOpaque() {
+        return true;
+    }
+
+    @Override
+    public void yield() {
+        // we cannot free the texture because we have no backup.
+    }
+
+    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/RawTexture.java b/src/com/android/gallery3d/ui/RawTexture.java
deleted file mode 100644
index c1be435..0000000
--- a/src/com/android/gallery3d/ui/RawTexture.java
+++ /dev/null
@@ -1,54 +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 javax.microedition.khronos.opengles.GL11;
-
-// 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 {
-
-    private RawTexture(GLCanvas canvas, int id) {
-        super(canvas, id, STATE_LOADED);
-    }
-
-    public static RawTexture newInstance(GLCanvas canvas) {
-        int[] textureId = new int[1];
-        GL11 gl = canvas.getGLInstance();
-        gl.glGenTextures(1, textureId, 0);
-        return new RawTexture(canvas, textureId[0]);
-    }
-
-    @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;
-    }
-
-    @Override
-    public void yield() {
-        // we cannot free the texture because we have no backup.
-    }
-}
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/PositionProvider.java b/src/com/android/gallery3d/ui/ScreenNailHolder.java
similarity index 62%
rename from src/com/android/gallery3d/ui/PositionProvider.java
rename to src/com/android/gallery3d/ui/ScreenNailHolder.java
index 930c61e..a7d5417 100644
--- a/src/com/android/gallery3d/ui/PositionProvider.java
+++ b/src/com/android/gallery3d/ui/ScreenNailHolder.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.
@@ -13,11 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.gallery3d.ui;
 
-import com.android.gallery3d.ui.PositionRepository.Position;
+import android.os.Parcel;
+import android.os.Parcelable;
 
-public interface PositionProvider {
-    public Position getPosition(long identity, Position target);
+public abstract class ScreenNailHolder implements Parcelable {
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+    }
+
+    public abstract ScreenNail attach();
+    public abstract void detach();
 }
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..1783b11 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);
@@ -135,14 +134,6 @@
         }
     }
 
-    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++) {
@@ -216,8 +207,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..bf3a55d 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,27 @@
                 (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();
+    }
+
+    public void startRestoringAnimation(int targetIndex) {
+        mAnimation = new RestoringAnimation(targetIndex);
+        mAnimation.start();
+        if (mLayout.mSlotCount != 0) invalidate();
     }
 
     private void updateScrollPosition(int position, boolean force) {
@@ -205,20 +201,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 +235,22 @@
         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);
         int oldX = mScrollX;
         updateScrollPosition(mScroller.getPosition(), false);
 
@@ -279,51 +273,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 +316,80 @@
         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);
+        }
+    }
+
+    public static class RestoringAnimation extends SlotAnimation {
+        private static final int DISTANCE = 1000;
+        private int mTargetIndex;
+
+        public RestoringAnimation(int targetIndex) {
+            mTargetIndex = targetIndex;
+        }
+
+        @Override
+        public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+            if (slotIndex == mTargetIndex) {
+                canvas.translate(0, 0, -DISTANCE * (1 - mProgress));
+            }
         }
     }
 
@@ -413,24 +411,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;
@@ -464,7 +447,7 @@
             return vPadding != mVerticalPadding || hPadding != mHorizontalPadding;
         }
 
-        public Rect getSlotRect(int index) {
+        public Rect getSlotRect(int index, Rect rect) {
             int col, row;
             if (WIDE) {
                 col = index / mUnitCount;
@@ -476,7 +459,8 @@
 
             int x = mHorizontalPadding + col * (mSlotWidth + mSlotGap);
             int y = mVerticalPadding + row * (mSlotHeight + mSlotGap);
-            return new Rect(x, y, x + mSlotWidth, y + mSlotHeight);
+            rect.set(x, y, x + mSlotWidth, y + mSlotHeight);
+            return rect;
         }
 
         public int getSlotWidth() {
@@ -487,10 +471,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,6 +519,10 @@
                 mSlotWidth = mSlotHeight;
             }
 
+            if (mRenderer != null) {
+                mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight);
+            }
+
             int[] padding = new int[2];
             if (WIDE) {
                 initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
@@ -592,6 +576,9 @@
             } else {
                 mVisibleStart = mVisibleEnd = 0;
             }
+            if (mRenderer != null) {
+                mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd);
+            }
         }
 
         public int getVisibleStart() {
@@ -645,26 +632,31 @@
         }
     }
 
-    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 +667,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 +680,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 +693,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 +702,7 @@
 
         @Override
         public void onLongPress(MotionEvent e) {
-            cancelDown();
+            cancelDown(true);
             if (mDownInScrolling) return;
             lockRendering();
             try {
@@ -735,7 +727,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;
     }
 
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..f7a3099
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.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..b651c69
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TextureUploader.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.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() {
+        mFgTextures.clear();
+        mBgTextures.clear();
+    }
+
+    // caller should hold synchronized on "this"
+    private void queueSelfIfNeed() {
+        if (mIsQueued) return;
+        mIsQueued = true;
+        mGLRoot.addOnGLIdleListener(this);
+    }
+
+    public synchronized void addBgTexture(UploadedTexture t) {
+        mBgTextures.addLast(t);
+        queueSelfIfNeed();
+    }
+
+    public synchronized void addFgTexture(UploadedTexture t) {
+        mFgTextures.addLast(t);
+        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();
+            }
+            if (!t.isContentValid(canvas)) {
+                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..b37cf9c 100644
--- a/src/com/android/gallery3d/ui/TileImageView.java
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -20,6 +20,8 @@
 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;
@@ -29,9 +31,6 @@
 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 {
@@ -72,7 +71,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 +79,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,7 +93,7 @@
     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();
@@ -123,7 +122,7 @@
 
     public static interface Model {
         public int getLevelCount();
-        public Bitmap getBackupImage();
+        public ScreenNail getScreenNail();
         public int getImageWidth();
         public int getImageHeight();
 
@@ -154,31 +153,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();
@@ -249,14 +236,15 @@
 
         // 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 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)) {
-                iter.remove();
+                mActiveTiles.removeAt(i);
+                i--;
+                n--;
                 recycleTile(tile);
             }
         }
@@ -277,7 +265,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 +297,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;
@@ -374,11 +364,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 +381,7 @@
                 tile = mRecycledQueue.pop();
             }
         }
-        updateBackupTexture(null);
+        setScreenNail(null);
     }
 
     public void prepareTextures() {
@@ -399,7 +391,7 @@
         if (mIsTextureFreed) {
             layoutTiles(mCenterX, mCenterY, mScale, mRotation);
             mIsTextureFreed = false;
-            updateBackupTexture(mModel != null ? mModel.getBackupImage() : null);
+            setScreenNail(mModel == null ? null : mModel.getScreenNail());
         }
     }
 
@@ -418,14 +410,18 @@
             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 (mScreenNail != null) {
+                    mScreenNail.noDraw();
+                }
+
                 int size = (TILE_SIZE << level);
                 float length = size * mScale;
                 Rect r = mTileRange;
@@ -437,8 +433,8 @@
                         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));
             }
@@ -455,7 +451,9 @@
 
     private void uploadBackgroundTiles(GLCanvas canvas) {
         mBackgroundTileUploaded = true;
-        for (Tile tile : mActiveTiles.values()) {
+        int n = mActiveTiles.size();
+        for (int i = 0; i < n; i++) {
+            Tile tile = mActiveTiles.valueAt(i);
             if (!tile.isContentValid(canvas)) queueForDecode(tile);
         }
     }
@@ -515,7 +513,7 @@
     }
 
     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 +529,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) {
@@ -587,14 +586,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);
         }
     }
 
diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
index be255d2..f4c6e65 100644
--- a/src/com/android/gallery3d/ui/TileImageViewAdapter.java
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -27,18 +27,21 @@
 
 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,7 +49,7 @@
     }
 
     public synchronized void clear() {
-        mBackupImage = null;
+        updateScreenNail(null, false);
         mImageWidth = 0;
         mImageHeight = 0;
         mLevelCount = 0;
@@ -54,8 +57,9 @@
         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;
@@ -63,6 +67,25 @@
         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;
+        mFailedToLoad = false;
+    }
+
+    private void updateScreenNail(ScreenNail screenNail, boolean own) {
+        if (mScreenNail != null && mOwnScreenNail) {
+            mScreenNail.recycle();
+        }
+        mScreenNail = screenNail;
+        mOwnScreenNail = own;
+    }
+
     public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) {
         mRegionDecoder = Utils.checkNotNull(decoder);
         mImageWidth = decoder.getWidth();
@@ -73,7 +96,7 @@
 
     private int calculateLevelCount() {
         return Math.max(0, Utils.ceilLog2(
-                (float) mImageWidth / mBackupImage.getWidth()));
+                (float) mImageWidth / mScreenNail.getWidth()));
     }
 
     @Override
@@ -139,8 +162,8 @@
     }
 
     @Override
-    public Bitmap getBackupImage() {
-        return mBackupImage;
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
     }
 
     @Override
diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java
index 1777048..85aa1c4 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;
 
@@ -227,7 +228,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);
@@ -298,6 +299,11 @@
         return isContentValid(canvas);
     }
 
+    @Override
+    protected int getTarget() {
+        return GL11.GL_TEXTURE_2D;
+    }
+
     public void setOpaque(boolean isOpaque) {
         mOpaque = isOpaque;
     }
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..13e08f9 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;
@@ -324,35 +325,23 @@
         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();
         int h = item.getHeight();
         return (h > 0 && w / h >= 2);
     }
+
+    public static Intent getHelpIntent(int helpUrlResId, Context context) {
+        Resources res = context.getResources();
+        String url = res.getString(helpUrlResId)
+                + "&hl=" + res.getConfiguration().locale.getLanguage();
+
+        Intent i = new Intent(Intent.ACTION_VIEW);
+        i.setData(Uri.parse(url));
+        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+        return i;
+    }
 }
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..8e60f59 100644
--- a/src/com/android/gallery3d/util/MediaSetUtils.java
+++ b/src/com/android/gallery3d/util/MediaSetUtils.java
@@ -16,12 +16,12 @@
 
 package com.android.gallery3d.util;
 
+import android.os.Environment;
+
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.MtpContext;
 import com.android.gallery3d.data.Path;
 
-import android.os.Environment;
-
 import java.util.Comparator;
 
 public class MediaSetUtils {
@@ -34,6 +34,9 @@
     public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId(
             Environment.getExternalStorageDirectory().toString() + "/"
             + MtpContext.NAME_IMPORTED_FOLDER);
+    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
index 6ccc264..1e0534e 100644
--- a/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java
+++ b/src/com/android/gallery3d/util/SpinnerVisibilitySetter.java
@@ -21,6 +21,7 @@
 import android.os.Message;
 import android.os.SystemClock;
 
+import java.lang.ref.WeakReference;
 import java.util.WeakHashMap;
 
 /**
@@ -47,25 +48,29 @@
             new WeakHashMap<Activity, SpinnerVisibilitySetter>();
 
     private long mSpinnerVisibilityStartTime = -1;
-    private Activity mActivity;
+    private WeakReference<Activity> mActivityRef;
 
     private Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
+            Activity activity = mActivityRef.get();
+
             switch(msg.what) {
                 case MSG_SHOW_SPINNER:
                     removeMessages(MSG_SHOW_SPINNER);
+                    if (activity == null) break;
                     if (mSpinnerVisibilityStartTime >= 0) break;
                     mSpinnerVisibilityStartTime = SystemClock.elapsedRealtime();
-                    mActivity.setProgressBarIndeterminateVisibility(true);
+                    activity.setProgressBarIndeterminateVisibility(true);
                     break;
                 case MSG_HIDE_SPINNER:
                     removeMessages(MSG_HIDE_SPINNER);
+                    if (activity == null) break;
                     if (mSpinnerVisibilityStartTime < 0) break;
                     long t = SystemClock.elapsedRealtime() - mSpinnerVisibilityStartTime;
                     if (t >= MIN_SPINNER_DISPLAY_TIME) {
                         mSpinnerVisibilityStartTime = -1;
-                        mActivity.setProgressBarIndeterminateVisibility(false);
+                        activity.setProgressBarIndeterminateVisibility(false);
                     } else {
                         sendEmptyMessageDelayed(MSG_HIDE_SPINNER, MIN_SPINNER_DISPLAY_TIME - t);
                     }
@@ -91,7 +96,8 @@
     }
 
     private SpinnerVisibilitySetter(Activity activity) {
-        mActivity = activity;
+        // Activity are keys. Value objects should not strongly refer to keys.
+        mActivityRef = new WeakReference<Activity>(activity);
     }
 
     public void setSpinnerVisibility(boolean visible) {
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..ee43cb9 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,5 @@
     }
     public void deleteRecycledResources() {}
     public void multiplyMatrix(float[] mMatrix, int offset) {}
+    public void dumpStatisticsAndClear() {}
 }
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/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..be2356c 100644
--- a/tests/src/com/android/gallery3d/ui/TextureTest.java
+++ b/tests/src/com/android/gallery3d/ui/TextureTest.java
@@ -43,6 +43,11 @@
             return true;
         }
 
+        @Override
+        protected int getTarget() {
+            return GL11.GL_TEXTURE_2D;
+        }
+
         public boolean isOpaque() {
             mOpaqueCalled++;
             return true;
@@ -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);
@@ -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);
+    }
+}