Merge changes I1acef9fc,I53ddb3cd

* changes:
  Make top level albums can be loaded concurrently.
  Add log to debug loading time.
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
index 03f522f..9a1d3d3 100644
--- a/src/com/android/gallery3d/app/AbstractGalleryActivity.java
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -43,6 +43,7 @@
     private static final String TAG = "AbstractGalleryActivity";
     private GLRootView mGLRootView;
     private StateManager mStateManager;
+    private GalleryActionBar mActionBar;
 
     private AlertDialog mAlertDialog = null;
     private BroadcastReceiver mMountReceiver = new BroadcastReceiver() {
@@ -192,7 +193,10 @@
 
     @Override
     public GalleryActionBar getGalleryActionBar() {
-        return null;
+        if (mActionBar == null) {
+            mActionBar = new GalleryActionBar(this);
+        }
+        return mActionBar;
     }
 
     @Override
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index 411464e..818a2ff 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -19,6 +19,7 @@
 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.Vibrator;
@@ -118,7 +119,7 @@
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
 
-            int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+            int slotViewTop = mActivity.getGalleryActionBar().getHeight();
             int slotViewBottom = bottom - top;
             int slotViewRight = right - left;
 
@@ -131,6 +132,7 @@
             mAlbumView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
             GalleryUtils.setViewPointMatrix(mMatrix,
                     (right - left) / 2, (bottom - top) / 2, -mUserDistance);
+            // Reset position offset after the layout is changed.
             PositionRepository.getInstance(mActivity).setOffset(
                     0, slotViewTop);
         }
@@ -168,7 +170,7 @@
         mAlbumView.invalidate();
     }
 
-    public void onSingleTapUp(int slotIndex) {
+    private void onSingleTapUp(int slotIndex) {
         MediaItem item = mAlbumDataAdapter.get(slotIndex);
         if (item == null) {
             Log.w(TAG, "item not ready yet, ignore the click");
@@ -185,6 +187,8 @@
                 Bundle data = new Bundle();
                 mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
                 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,
@@ -199,6 +203,16 @@
         }
     }
 
+    private Rect getSlotRect(int slotIndex) {
+        // Get slot rectangle relative to this root pane.
+        Rect offset = new Rect();
+        mRootPane.getBoundsOf(mAlbumView, offset);
+        Rect r = mAlbumView.getSlotRect(slotIndex);
+        r.offset(offset.left - mAlbumView.getScrollX(),
+                offset.top - mAlbumView.getScrollY());
+        return r;
+    }
+
     private void onGetContent(final MediaItem item) {
         DataManager dm = mActivity.getDataManager();
         Activity activity = (Activity) mActivity;
@@ -315,6 +329,9 @@
         super.onResume();
         mIsActive = true;
         setContentPane(mRootPane);
+        // Reset position offset for resuming.
+        PositionRepository.getInstance(mActivity).setOffset(
+                mAlbumView.bounds().left, mAlbumView.bounds().top);
 
         Path path = mMediaSet.getPath();
         boolean enableHomeButton = (mActivity.getStateManager().getStateCount() > 1) |
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
index c52cd8c..d00c03c 100644
--- a/src/com/android/gallery3d/app/AlbumSetPage.java
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -79,6 +79,7 @@
     private String mTitle;
     private String mSubtitle;
     private boolean mShowClusterMenu;
+    private GalleryActionBar mActionBar;
     private int mSelectedAction;
     private Vibrator mVibrator;
 
@@ -120,7 +121,7 @@
                 boolean changed, int left, int top, int right, int bottom) {
             mEyePosition.resetPosition();
 
-            int slotViewTop = GalleryActionBar.getHeight((Activity) mActivity);
+            int slotViewTop = mActionBar.getHeight();
             int slotViewBottom = bottom - top;
             int slotViewRight = right - left;
 
@@ -131,6 +132,7 @@
             }
 
             mAlbumSetView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+            // Reset position offset after the layout is changed.
             PositionRepository.getInstance(mActivity).setOffset(
                     0, slotViewTop);
         }
@@ -270,11 +272,9 @@
         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);
-        }
+        mActionBar = mActivity.getGalleryActionBar();
+        mSelectedAction = data.getInt(AlbumSetPage.KEY_SELECTED_CLUSTER_TYPE,
+                FilterUtils.CLUSTER_BY_ALBUM);
         startTransition();
     }
 
@@ -311,11 +311,10 @@
         mAlbumSetView.pause();
         mEyePosition.pause();
         DetailsHelper.pause();
-        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
         // 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.
-        if (actionBar != null) actionBar.disableClusterMenu(false);
+        mActionBar.disableClusterMenu(false);
         if (mSyncTask != null) {
             mSyncTask.cancel();
             mSyncTask = null;
@@ -329,6 +328,9 @@
         super.onResume();
         mIsActive = true;
         setContentPane(mRootPane);
+        // Reset position offset for resuming.
+        PositionRepository.getInstance(mActivity).setOffset(
+                mAlbumSetView.bounds().left, mAlbumSetView.bounds().top);
 
         // Set the reload bit here to prevent it exit this page in clearLoadingBit().
         setLoadingBit(BIT_LOADING_RELOAD);
@@ -337,9 +339,8 @@
         mAlbumSetView.resume();
         mEyePosition.resume();
         mActionModeHandler.resume();
-        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
-        if (mShowClusterMenu && actionBar != null) {
-            actionBar.enableClusterMenu(mSelectedAction, this);
+        if (mShowClusterMenu) {
+            mActionBar.enableClusterMenu(mSelectedAction, this);
         }
         if (!mInitialSynced) {
             setLoadingBit(BIT_LOADING_SYNC);
@@ -399,7 +400,6 @@
     @Override
     protected boolean onCreateActionBar(Menu menu) {
         Activity activity = (Activity) mActivity;
-        GalleryActionBar actionBar = mActivity.getGalleryActionBar();
         MenuInflater inflater = activity.getMenuInflater();
 
         final boolean inAlbum = mActivity.getStateManager().hasStateClass(
@@ -415,10 +415,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);
@@ -426,7 +426,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 {
@@ -434,14 +434,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;
     }
@@ -525,9 +525,8 @@
     }
 
     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;
@@ -539,7 +538,7 @@
 
         switch (mode) {
             case SelectionManager.ENTER_SELECTION_MODE: {
-                mActivity.getGalleryActionBar().disableClusterMenu(true);
+                mActionBar.disableClusterMenu(true);
                 mActionMode = mActionModeHandler.startActionMode();
                 mVibrator.vibrate(100);
                 break;
@@ -547,7 +546,7 @@
             case SelectionManager.LEAVE_SELECTION_MODE: {
                 mActionMode.finish();
                 if (mShowClusterMenu) {
-                    mActivity.getGalleryActionBar().enableClusterMenu(mSelectedAction, this);
+                    mActionBar.enableClusterMenu(mSelectedAction, this);
                 }
                 mRootPane.invalidate();
                 break;
diff --git a/src/com/android/gallery3d/app/CameraView.java b/src/com/android/gallery3d/app/CameraView.java
new file mode 100644
index 0000000..a6c2332
--- /dev/null
+++ b/src/com/android/gallery3d/app/CameraView.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.View;
+
+import java.io.IOException;
+
+// This is a sample View which demos the usage of ScreenNailBridge. It
+// is not intended for production use.
+public class CameraView extends TextureView implements
+        TextureView.SurfaceTextureListener, ScreenNailBridge.Listener {
+    private static final String TAG = "CameraView";
+    private static final int PREVIEW_WIDTH = 960;
+    private static final int PREVIEW_HEIGHT = 720;
+    private Camera mCamera;
+    private ScreenNailBridge mScreenNailBridge;
+
+    public CameraView(Context context) {
+        super(context);
+        init();
+    }
+
+    public CameraView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    private void init() {
+        setVisibility(View.INVISIBLE);
+        setSurfaceTextureListener(this);
+    }
+
+    public void setScreenNailBridge(ScreenNailBridge s) {
+        mScreenNailBridge = s;
+    }
+
+    @Override
+    public void onMeasure(int widthSpec, int heightSpec) {
+        int width = getDefaultSize(PREVIEW_WIDTH, widthSpec);
+        int height = getDefaultSize(PREVIEW_HEIGHT, heightSpec);
+        // Keep aspect ratio
+        if (width * PREVIEW_HEIGHT > PREVIEW_WIDTH * height) {
+            width = PREVIEW_WIDTH * height / PREVIEW_HEIGHT;
+        } else {
+            height = PREVIEW_HEIGHT * width / PREVIEW_WIDTH;
+        }
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    public void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mScreenNailBridge.setSize(w, h);
+    }
+
+    @Override
+    public void updateView(boolean visible, int x, int y, int w, int h) {
+        if (!visible) {
+            setVisibility(View.INVISIBLE);
+        } else {
+            setVisibility(View.VISIBLE);
+            setTranslationX(x);
+            setTranslationY(y);
+        }
+    }
+
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+        try {
+            mCamera = Camera.open();
+
+            Camera.Parameters param = mCamera.getParameters();
+            param.setPreviewSize(PREVIEW_WIDTH, PREVIEW_HEIGHT);
+            mCamera.setParameters(param);
+
+            mCamera.setPreviewTexture(surface);
+            mCamera.startPreview();
+        } catch (Throwable ex) {
+            Log.e(TAG, "failed to open camera", ex);
+        }
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+        mCamera.stopPreview();
+        mCamera.release();
+        return true;
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+    }
+}
diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java
index cd93c45..e3937bc 100644
--- a/src/com/android/gallery3d/app/Gallery.java
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -52,7 +52,6 @@
     public static final String KEY_MEDIA_TYPES = "mediaTypes";
 
     private static final String TAG = "Gallery";
-    private GalleryActionBar mActionBar;
     private Dialog mVersionCheckDialog;
 
     @Override
@@ -63,7 +62,6 @@
         requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
 
         setContentView(R.layout.main);
-        mActionBar = new GalleryActionBar(this);
 
         if (savedInstanceState != null) {
             getStateManager().restoreFromState(savedInstanceState);
@@ -276,11 +274,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 35cb8b4..c99cd60 100644
--- a/src/com/android/gallery3d/app/GalleryActionBar.java
+++ b/src/com/android/gallery3d/app/GalleryActionBar.java
@@ -19,6 +19,7 @@
 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;
@@ -37,6 +38,16 @@
 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,41 +179,26 @@
         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);
-            }
-        }
-        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 enableClusterMenu(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;
+        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) {
-        mClusterRunner = null;
-        if (hideMenu) {
-            mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+        if (mActionBar != null) {
+            mClusterRunner = null;
+            if (hideMenu) {
+                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+            }
         }
     }
 
@@ -214,8 +217,7 @@
         if (mActionBar != null) {
             int options = (displayHomeAsUp ? ActionBar.DISPLAY_HOME_AS_UP : 0) |
                     (showTitle ? ActionBar.DISPLAY_SHOW_TITLE : 0);
-            mActionBar.setDisplayOptions(
-                    options,
+            mActionBar.setDisplayOptions(options,
                     ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
             mActionBar.setHomeButtonEnabled(displayHomeAsUp);
         }
@@ -233,7 +235,25 @@
         if (mActionBar != null) mActionBar.setSubtitle(title);
     }
 
+    public void show() {
+        if (mActionBar != null) mActionBar.show();
+    }
+
+    public void hide() {
+        if (mActionBar != null) mActionBar.hide();
+    }
+
+    public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        if (mActionBar != null) mActionBar.addOnMenuVisibilityListener(listener);
+    }
+
+    public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+        if (mActionBar != null) mActionBar.removeOnMenuVisibilityListener(listener);
+    }
+
     public boolean setSelectedAction(int type) {
+        if (mActionBar == null) return false;
+
         for (int i = 0, n = sClusterItems.length; i < n; i++) {
             ActionItem item = sClusterItems[i];
             if (item.action == type) {
diff --git a/src/com/android/gallery3d/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java
index 4134e32..124eb7bf 100644
--- a/src/com/android/gallery3d/app/ManageCachePage.java
+++ b/src/com/android/gallery3d/app/ManageCachePage.java
@@ -112,7 +112,7 @@
 
             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);
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
index 0b544b9..82225d9 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -29,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;
@@ -215,20 +216,22 @@
         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();
+        entry.screenNail = screenNail;
 
-        if (entry.screenNail == null) {
+        if (screenNail == null) {
             entry.failToLoad = true;
         }
+
         if (mDataListener != null) {
             mDataListener.onPhotoAvailable(version, false);
         }
@@ -291,24 +294,19 @@
         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;
     }
 
-    public ImageData getPreviousImage() {
+    public ScreenNail getPrevScreenNail() {
         return getImage(mCurrentIndex - 1);
     }
 
-    public ImageData getNextImage() {
+    public ScreenNail getNextScreenNail() {
         return getImage(mCurrentIndex + 1);
     }
 
@@ -343,8 +341,8 @@
         updateCurrentIndex(index);
     }
 
-    public Bitmap getBackupImage() {
-        return mTileProvider.getBackupImage();
+    public ScreenNail getScreenNail() {
+        return mTileProvider.getScreenNail();
     }
 
     public int getImageHeight() {
@@ -409,17 +407,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();
@@ -489,7 +487,7 @@
         }
     }
 
-    private static class ScreenNailJob implements Job<Bitmap> {
+    private static class ScreenNailJob implements Job<ScreenNail> {
         private MediaItem mItem;
 
         public ScreenNailJob(MediaItem item) {
@@ -497,14 +495,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, mItem.getFullImageRotation());
         }
     }
 
@@ -604,16 +607,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));
@@ -629,8 +632,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;
     }
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index 8040a2f..7daf5df 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -16,13 +16,13 @@
 
 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.os.Bundle;
 import android.os.Handler;
@@ -31,6 +31,7 @@
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.View.MeasureSpec;
 import android.view.WindowManager;
 import android.widget.ShareActionProvider;
@@ -44,6 +45,7 @@
 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;
@@ -77,6 +79,7 @@
     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";
 
     private GalleryApp mApplication;
     private SelectionManager mSelectionManager;
@@ -97,7 +100,7 @@
     private int mCurrentIndex = 0;
     private Handler mHandler;
     private boolean mShowBars = true;
-    private ActionBar mActionBar;
+    private GalleryActionBar mActionBar;
     private MyMenuVisibilityListener mMenuVisibilityListener;
     private boolean mIsMenuVisible;
     private boolean mIsInteracting;
@@ -107,6 +110,11 @@
     private ShareActionProvider mShareActionProvider;
     private String mSetPathString;
 
+    // This is for testing only. It should be removed once we have the real
+    // Camera view.
+    private CameraView mCameraView;
+    private ScreenNailBridge mScreenNail;
+
     public static interface Model extends PhotoView.Model {
         public void resume();
         public void pause();
@@ -134,6 +142,7 @@
         protected void onLayout(
                 boolean changed, int left, int top, int right, int bottom) {
             mPhotoView.layout(0, 0, right - left, bottom - top);
+            // Reset position offset after the layout is changed.
             PositionRepository.getInstance(mActivity).setOffset(0, 0);
             int filmStripHeight = 0;
             if (mFilmStripView != null) {
@@ -145,8 +154,7 @@
                         right - left, bottom - top);
             }
             if (mShowDetails) {
-                mDetailsHelper.layout(left, GalleryActionBar.getHeight((Activity) mActivity),
-                        right, bottom);
+                mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
             }
         }
     };
@@ -169,7 +177,7 @@
 
     @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);
 
@@ -182,6 +190,14 @@
         Path itemPath = Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH));
 
         if (mSetPathString != null) {
+            // Uncomment the block below to test camera screennail.
+            /*
+            Path cameraScreenNailSetPath = addCameraScreenNail();
+
+            // Combine the original MediaSet with the one for camera ScreenNail.
+            mSetPathString = "/combo/item/{" + cameraScreenNailSetPath + "," +
+                    mSetPathString + "}";
+            */
             mMediaSet = mActivity.getDataManager().getMediaSet(mSetPathString);
             mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
             mMediaSet = (MediaSet)
@@ -257,8 +273,38 @@
             }
         };
 
-        // 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));
+        }
+    }
+
+    // We create a Camera View and a ScreenNail. The two work together
+    // to present the view together with other pictures. Returns the
+    // Path of the MediaItem hosting the ScreenNail.
+    private Path addCameraScreenNail() {
+        // Create a camera view and add it to the root.
+        Activity activity = (Activity) mActivity;
+        mCameraView = new CameraView(activity);
+        ViewGroup galleryRoot = (ViewGroup) activity.findViewById(R.id.gallery_root);
+        galleryRoot.addView(mCameraView);
+
+        // Create a ScreenNail and register it.
+        mScreenNail = new ScreenNailBridge(mCameraView);
+        mCameraView.setScreenNailBridge(mScreenNail);
+        return SnailSource.registerScreenNail(mScreenNail);
+    }
+
+    private void removeCameraScreenNail() {
+        if (mCameraView == null) return;
+
+        // Remove the camera view.
+        ((ViewGroup) mCameraView.getParent()).removeView(mCameraView);
+        mCameraView = null;
+
+        // Unregister the ScreenNail.
+        SnailSource.unregisterScreenNail(mScreenNail);
+        mScreenNail = null;
     }
 
     private void updateShareURI(Path path) {
@@ -298,7 +344,9 @@
         mPhotoView.showVideoPlayIcon(
                 photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO);
 
-        updateShareURI(photo.getPath());
+        if ((photo.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
+            updateShareURI(photo.getPath());
+        }
     }
 
     private void updateMenuOperations() {
@@ -622,6 +670,10 @@
         super.onResume();
         mIsActive = true;
         setContentPane(mRootPane);
+        // Reset position offset for resuming.
+        PositionRepository.getInstance(mActivity).setOffset(
+                mPhotoView.bounds().left, mPhotoView.bounds().top);
+
         mModel.resume();
         mPhotoView.resume();
         if (mFilmStripView != null) {
@@ -630,12 +682,17 @@
         if (mMenuVisibilityListener == null) {
             mMenuVisibilityListener = new MyMenuVisibilityListener();
         }
-        mActivity.getGalleryActionBar().setDisplayOptions(mSetPathString != null, true);
-
+        mActionBar.setDisplayOptions(mSetPathString != null, true);
         mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
         onUserInteraction();
     }
 
+    @Override
+    protected void onDestroy() {
+        removeCameraScreenNail();
+        super.onDestroy();
+    }
+
     private class MyDetailsSource implements DetailsSource {
         private int mIndex;
 
diff --git a/src/com/android/gallery3d/app/PickerActivity.java b/src/com/android/gallery3d/app/PickerActivity.java
index 855a0ce..d63e237 100644
--- a/src/com/android/gallery3d/app/PickerActivity.java
+++ b/src/com/android/gallery3d/app/PickerActivity.java
@@ -33,8 +33,6 @@
 
     public static final String KEY_ALBUM_PATH = "album-path";
 
-    private GalleryActionBar mActionBar;
-
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -46,7 +44,6 @@
         if (!isDialog) {
             requestWindowFeature(Window.FEATURE_ACTION_BAR);
             requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
-            mActionBar = new GalleryActionBar(this);
         }
 
         setContentView(R.layout.dialog_picker);
@@ -96,9 +93,4 @@
     public void onClick(View v) {
         if (v.getId() == R.id.cancel) finish();
     }
-
-    @Override
-    public GalleryActionBar getGalleryActionBar() {
-        return mActionBar;
-    }
 }
diff --git a/src/com/android/gallery3d/app/ScreenNailBridge.java b/src/com/android/gallery3d/app/ScreenNailBridge.java
new file mode 100644
index 0000000..9da197d
--- /dev/null
+++ b/src/com/android/gallery3d/app/ScreenNailBridge.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import android.graphics.RectF;
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.ScreenNail;
+
+// This is a ScreenNail whose actually display is done by an foreign component.
+// The foreign component tells the ScreenNail its size by setSize(). The
+// ScreenNail tells the foreign component the position to display by
+// updateView().
+class ScreenNailBridge implements ScreenNail {
+    private static final String TAG = "ScreenNailBridge";
+    private int mWidth, mHeight;
+    private boolean mVisible = false;
+    private int mDrawX, mDrawY, mDrawWidth, mDrawHeight;
+    private Listener mListener;
+    private Handler mMainHandler;
+
+    public interface Listener {
+        // This is called from the main thread.
+        void updateView(boolean visible, int x, int y, int width, int height);
+    };
+
+    // The constructor should be called from the main thread.
+    public ScreenNailBridge(Listener listener) {
+        mListener = listener;
+        mMainHandler = new Handler();
+    }
+
+    // This can be called from any thread.  (We expect it to be called from the
+    // main thread).
+    public synchronized void setSize(int w, int h) {
+        mWidth = w;
+        mHeight = h;
+    }
+
+    // This can be called from any thread. (We expect it to be called from GL
+    // thread)
+    @Override
+    public synchronized int getWidth() {
+        return mWidth;
+    }
+
+    // This can be called from any thread. (We expect it to be called from GL
+    // thread)
+    @Override
+    public synchronized int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public int getRotation() {
+        return 0;
+    }
+
+    // This is run in the main thread.
+    private Runnable mUpdateViewRunnable = new Runnable() {
+            public void run() {
+                boolean v;
+                int x, y, width, height;
+                synchronized (ScreenNailBridge.this) {
+                    v = mVisible;
+                    x = mDrawX;
+                    y = mDrawY;
+                    width = mDrawWidth;
+                    height = mDrawHeight;
+                }
+                mListener.updateView(v, x, y, width, height);
+            }
+        };
+
+    @Override
+    public synchronized void draw(GLCanvas canvas, int x, int y, int width, int height) {
+        if (mVisible && mDrawX == x && mDrawY == y && mDrawWidth == width &&
+                mDrawHeight == height) {
+            return;
+        }
+        mVisible = true;
+        mDrawX = x;
+        mDrawY = y;
+        mDrawWidth = width;
+        mDrawHeight = height;
+        mMainHandler.post(mUpdateViewRunnable);
+    }
+
+    @Override
+    public synchronized void disableDraw() {
+        if (!mVisible) return;
+        mVisible = false;
+        mMainHandler.post(mUpdateViewRunnable);
+    }
+
+    @Override
+    public void recycle() {
+        // Make sure we will not draw anymore.
+        disableDraw();
+    }
+
+    @Override
+    public void draw(GLCanvas canvas, RectF source, RectF dest) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
index adad8fd..66e7c20 100644
--- a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -28,7 +28,7 @@
 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.ScreenNail;
 import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.ui.TileImageViewAdapter;
 import com.android.gallery3d.util.Future;
@@ -115,7 +115,7 @@
 
     private void onDecodeLargeComplete(ImageBundle bundle) {
         try {
-            setBackupImage(bundle.backupImage,
+            setScreenNail(bundle.backupImage,
                     bundle.decoder.getWidth(), bundle.decoder.getHeight());
             setRegionDecoder(bundle.decoder);
             mPhotoView.notifyImageInvalidated(0);
@@ -128,7 +128,7 @@
         try {
             Bitmap backup = future.get();
             if (backup == null) return;
-            setBackupImage(backup, backup.getWidth(), backup.getHeight());
+            setScreenNail(backup, backup.getWidth(), backup.getHeight());
             mPhotoView.notifyOnNewImage();
             mPhotoView.notifyImageInvalidated(0); // the current image
         } catch (Throwable t) {
@@ -158,11 +158,11 @@
         }
     }
 
-    public ImageData getNextImage() {
+    public ScreenNail getNextScreenNail() {
         return null;
     }
 
-    public ImageData getPreviousImage() {
+    public ScreenNail getPrevScreenNail() {
         return null;
     }
 
diff --git a/src/com/android/gallery3d/data/DataManager.java b/src/com/android/gallery3d/data/DataManager.java
index e6e97e9..1da3b76 100644
--- a/src/com/android/gallery3d/data/DataManager.java
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -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()) {
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
index b682c2d..b2632f1 100644
--- a/src/com/android/gallery3d/data/MediaItem.java
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -20,6 +20,7 @@
 import android.graphics.BitmapRegionDecoder;
 
 import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.ui.ScreenNail;
 
 // MediaItem represents an image or a video item.
 public abstract class MediaItem extends MediaObject {
@@ -90,6 +91,12 @@
     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:
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..17b899d
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailSource.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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;
+    }
+
+    // Register a ScreenNail. Returns the Path of the MediaSet
+    // containing the MediaItem associated with the ScreenNail.
+    public static synchronized Path registerScreenNail(ScreenNail s) {
+        int id = sNextId++;
+        sRegistry.put(id, s);
+        return Path.fromString("/snail/set").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/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
new file mode 100644
index 0000000..117a9ac
--- /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;
+
+// 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 final int mRotation;
+    private final Bitmap mBitmap;
+    private BitmapTexture mTexture;
+
+    public BitmapScreenNail(Bitmap bitmap, int rotation) {
+        mWidth = bitmap.getWidth();
+        mHeight = bitmap.getHeight();
+        mRotation = rotation;
+        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 int getRotation() {
+        return mRotation;
+    }
+
+    @Override
+    public void recycle() {
+        if (mTexture != null) {
+            mTexture.recycle();
+        }
+    }
+
+    @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 disableDraw() {
+    }
+
+    @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/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java
index ec2f97f..a031b85 100644
--- a/src/com/android/gallery3d/ui/BitmapTileProvider.java
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java
@@ -25,7 +25,7 @@
 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), 0);
         mMipmaps = list.toArray(new Bitmap[list.size()]);
         mConfig = Config.ARGB_8888;
     }
 
-    public Bitmap getBackupImage() {
-        return mBackup;
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
     }
 
     public int getImageHeight() {
@@ -78,7 +78,9 @@
         for (Bitmap bitmap : mMipmaps) {
             BitmapUtils.recycleSilently(bitmap);
         }
-        BitmapUtils.recycleSilently(mBackup);
+        if (mScreenNail != null) {
+            mScreenNail.recycle();
+        }
     }
 
     public boolean isFailedToLoad() {
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index 31e41bf..3ec245f 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -19,14 +19,13 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
+import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Message;
 import android.view.MotionEvent;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryActivity;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.ui.PositionRepository.Position;
 
 public class PhotoView extends GLView {
     @SuppressWarnings("unused")
@@ -91,11 +90,9 @@
 
     private int mImageRotation;
 
-    private Path mOpenedItemPath;
-    private GalleryActivity mActivity;
+    private Rect mOpenAnimationRect;
 
     public PhotoView(GalleryActivity activity) {
-        mActivity = activity;
         mTileView = new TileImageView(activity);
         addComponent(mTileView);
         Context context = activity.getAndroidContext();
@@ -120,7 +117,7 @@
                     case MSG_SHOW_LOADING: {
                         if (mLoadingState == LOADING_INIT) {
                             // We don't need the opening animation
-                            mOpenedItemPath = null;
+                            mOpenAnimationRect = null;
 
                             mLoadingSpinner.startAnimation();
                             mLoadingState = LOADING_TIMEOUT;
@@ -176,18 +173,14 @@
         }
     }
 
-    private void updateScreenNailEntry(int which, ImageData data) {
+    private void updateScreenNailEntry(int which, ScreenNail screenNail) {
         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);
-        }
+        entry.set(screenNail);
     }
 
     // -1 previous, 0 current, 1 next
@@ -195,13 +188,13 @@
         switch (which) {
             case -1: {
                 updateScreenNailEntry(
-                        ENTRY_PREVIOUS, mModel.getPreviousImage());
+                        ENTRY_PREVIOUS, mModel.getPrevScreenNail());
                 layoutScreenNails();
                 invalidate();
                 break;
             }
             case 1: {
-                updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
+                updateScreenNailEntry(ENTRY_NEXT, mModel.getNextScreenNail());
                 layoutScreenNails();
                 invalidate();
                 break;
@@ -230,14 +223,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
-            mOpenedItemPath = null;
+            mOpenAnimationRect = null;
         } else if (mLoadingState != LOADING_INIT) {
             mLoadingState = LOADING_INIT;
             mHandler.removeMessages(MSG_SHOW_LOADING);
@@ -251,8 +244,8 @@
             updateScreenNailEntry(ENTRY_PREVIOUS, null);
             updateScreenNailEntry(ENTRY_NEXT, null);
         } else {
-            updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPreviousImage());
-            updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
+            updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPrevScreenNail());
+            updateScreenNailEntry(ENTRY_NEXT, mModel.getNextScreenNail());
         }
         layoutScreenNails();
 
@@ -344,8 +337,8 @@
             ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
             ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
 
-            if (prevNail.mVisible) prevNail.draw(canvas);
-            if (nextNail.mVisible) nextNail.draw(canvas);
+            prevNail.draw(canvas);
+            nextNail.draw(canvas);
         }
 
         // Draw the progress spinner and the text below it
@@ -581,10 +574,10 @@
         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;
+        if (prevNail.mScreenNail != null) prevNail.mScreenNail.recycle();
+        prevNail.set(mTileView.mScreenNail);
+        mTileView.updateScreenNail(nextNail.mScreenNail);
+        nextNail.set(null);
         mModel.next();
     }
 
@@ -593,10 +586,10 @@
         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;
+        if (nextNail.mScreenNail != null) nextNail.mScreenNail.recycle();
+        nextNail.set(mTileView.mScreenNail);
+        mTileView.updateScreenNail(prevNail.mScreenNail);
+        nextNail.set(null);
         mModel.previous();
     }
 
@@ -627,18 +620,8 @@
         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;
-        }
+        public ScreenNail getNextScreenNail();
+        public ScreenNail getPrevScreenNail();
     }
 
     private static int getRotated(int degree, int original, int theother) {
@@ -649,28 +632,20 @@
         private boolean mVisible;
         private boolean mEnabled;
 
-        private int mRotation;
         private int mDrawWidth;
         private int mDrawHeight;
         private int mOffsetX;
+        private int mRotation;
 
-        private BitmapTexture mTexture;
+        private ScreenNail mScreenNail;
 
-        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);
-                }
+        public void set(ScreenNail screenNail) {
+            mEnabled = (screenNail != null);
+            if (mScreenNail == screenNail) return;
+            if (mScreenNail != null) mScreenNail.recycle();
+            mScreenNail = screenNail;
+            if (mScreenNail != null) {
+                mRotation = mScreenNail.getRotation();
                 updateDrawingSize();
             }
         }
@@ -689,15 +664,15 @@
 
         public int gapToSide() {
             return ((mRotation / 90) & 1) != 0
-                    ? PhotoView.gapToSide(mDrawHeight, getWidth())
-                    : PhotoView.gapToSide(mDrawWidth, getWidth());
+                ? PhotoView.gapToSide(mDrawHeight, getWidth())
+                : PhotoView.gapToSide(mDrawWidth, getWidth());
         }
 
         public void updateDrawingSize() {
-            if (mTexture == null) return;
+            if (mScreenNail == null) return;
 
-            int width = mTexture.getWidth();
-            int height = mTexture.getHeight();
+            int width = mScreenNail.getWidth();
+            int height = mScreenNail.getHeight();
 
             // Calculate the initial scale that will used by PositionController
             // (usually fit-to-screen)
@@ -714,21 +689,27 @@
         }
 
         public void draw(GLCanvas canvas) {
+            if (mScreenNail == null) return;
+            if (!mVisible) {
+                mScreenNail.disableDraw();
+                return;
+            }
+
             int x = mOffsetX;
             int y = getHeight() / 2;
 
-            if (mTexture != null) {
-                if (mRotation != 0) {
-                    canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
-                    canvas.translate(x, y);
-                    canvas.rotate(mRotation, 0, 0, 1); //mRotation
-                    canvas.translate(-x, -y);
-                }
-                mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2,
-                        mDrawWidth, mDrawHeight);
-                if (mRotation != 0) {
-                    canvas.restore();
-                }
+            if (mRotation != 0) {
+                canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+                canvas.translate(x, y);
+                canvas.rotate(mRotation, 0, 0, 1);
+                canvas.translate(-x, -y);
+            }
+
+            mScreenNail.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2,
+                    mDrawWidth, mDrawHeight);
+
+            if (mRotation != 0) {
+                canvas.restore();
             }
         }
     }
@@ -738,7 +719,7 @@
         mTransitionMode = TRANS_NONE;
         mTileView.freeTextures();
         for (ScreenNailEntry entry : mScreenNails) {
-            entry.set(false, null, 0);
+            entry.set(null);
         }
     }
 
@@ -746,24 +727,19 @@
         mTileView.prepareTextures();
     }
 
-    public void setOpenedItem(Path itemPath) {
-        mOpenedItemPath = itemPath;
+    public void setOpenAnimationRect(Rect rect) {
+        mOpenAnimationRect = rect;
     }
 
     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(
-                    System.identityHashCode(mOpenedItemPath));
-            mOpenedItemPath = null;
-            return position;
-        }
-        return null;
+    // Returns the opening animation rectangle saved by the previous page.
+    public Rect retrieveOpenAnimationRect() {
+        Rect r = mOpenAnimationRect;
+        mOpenAnimationRect = null;
+        return r;
     }
 
     public void openAnimationStarted() {
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
index 1c9aba8..09fbc17 100644
--- a/src/com/android/gallery3d/ui/PositionController.java
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -17,11 +17,12 @@
 package com.android.gallery3d.ui;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.graphics.RectF;
 import android.util.FloatMath;
 
 import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.util.GalleryUtils;
 
 class PositionController {
@@ -140,14 +141,20 @@
 
         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;
+        // Start animation from the saved rectangle if we have one.
+        Rect r = mViewer.retrieveOpenAnimationRect();
+        if (r != null) {
+            // The animation starts from the specified rectangle; the image
+            // should be scaled and centered as the thumbnail shown in the
+            // rectangle to minimize janky opening animation. Note: The below
+            // implementation depends on how thumbnails are drawn and placed.
+            float size = MediaItem.getTargetSize(
+                    MediaItem.TYPE_MICROTHUMBNAIL);
+            float scale = (size / Math.min(width, height)) * Math.min(
+                    r.width() / size, r.height() / size);
+
+            mCurrentX = Math.round((mViewW / 2f - r.centerX()) / scale) + mImageW / 2;
+            mCurrentY = Math.round((mViewH / 2f - r.centerY()) / scale) + mImageH / 2;
             mCurrentScale = scale;
             mViewer.openAnimationStarted();
             startSnapback();
diff --git a/src/com/android/gallery3d/ui/ScreenNail.java b/src/com/android/gallery3d/ui/ScreenNail.java
new file mode 100644
index 0000000..a2377fe
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScreenNail.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.ui;
+
+import android.graphics.RectF;
+
+public interface ScreenNail {
+    public int getWidth();
+    public int getHeight();
+    public int getRotation();
+    public void recycle();
+    public void draw(GLCanvas canvas, int x, int y, int width, int height);
+
+    // We need this method to tell ScreenNail to stop displaying.
+    public void disableDraw();
+
+    // 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/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
index 7ad5546..d81244d 100644
--- a/src/com/android/gallery3d/ui/TileImageView.java
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -70,7 +70,7 @@
     private static final int STATE_RECYCLED = 0x40;
 
     private Model mModel;
-    protected BitmapTexture mBackupImage;
+    protected ScreenNail mScreenNail;
     protected int mLevelCount;  // cache the value of mScaledBitmaps.length
 
     // The mLevel variable indicates which level of bitmap we should use.
@@ -78,7 +78,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)
@@ -120,7 +120,7 @@
 
     public static interface Model {
         public int getLevelCount();
-        public Bitmap getBackupImage();
+        public ScreenNail getScreenNail();
         public int getImageWidth();
         public int getImageHeight();
 
@@ -139,31 +139,21 @@
         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 updateScreenNail(ScreenNail s) {
+        if (mScreenNail == s) return;
+        if (mScreenNail != null) mScreenNail.recycle();
+        mScreenNail = s;
     }
 
     public void notifyModelInvalidated() {
         invalidateTiles();
         if (mModel == null) {
-            mBackupImage = null;
+            mScreenNail = null;
             mImageWidth = 0;
             mImageHeight = 0;
             mLevelCount = 0;
         } else {
-            updateBackupTexture(mModel.getBackupImage());
+            updateScreenNail(mModel.getScreenNail());
             mImageWidth = mModel.getImageWidth();
             mImageHeight = mModel.getImageHeight();
             mLevelCount = mModel.getLevelCount();
@@ -348,7 +338,7 @@
                 tile = mRecycledQueue.pop();
             }
         }
-        updateBackupTexture(null);
+        updateScreenNail(null);
     }
 
     public void prepareTextures() {
@@ -358,7 +348,7 @@
         if (mIsTextureFreed) {
             layoutTiles(mCenterX, mCenterY, mScale, mRotation);
             mIsTextureFreed = false;
-            updateBackupTexture(mModel != null ? mModel.getBackupImage() : null);
+            updateScreenNail(mModel != null ? mModel.getScreenNail() : null);
         }
     }
 
@@ -379,6 +369,10 @@
         }
         try {
             if (level != mLevelCount) {
+                if (mScreenNail != null) {
+                    mScreenNail.disableDraw();
+                }
+
                 int size = (TILE_SIZE << level);
                 float length = size * mScale;
                 Rect r = mTileRange;
@@ -390,8 +384,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));
             }
@@ -542,14 +536,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 63bb0b2..475184e 100644
--- a/src/com/android/gallery3d/ui/TileImageViewAdapter.java
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -27,10 +27,10 @@
 
 public class TileImageViewAdapter implements TileImageView.Model {
     private static final String TAG = "TileImageViewAdapter";
+    protected ScreenNail mScreenNail;
     protected BitmapRegionDecoder mRegionDecoder;
     protected int mImageWidth;
     protected int mImageHeight;
-    protected Bitmap mBackupImage;
     protected int mLevelCount;
     protected boolean mFailedToLoad;
 
@@ -40,8 +40,8 @@
     public TileImageViewAdapter() {
     }
 
-    public TileImageViewAdapter(Bitmap backup, BitmapRegionDecoder regionDecoder) {
-        mBackupImage = Utils.checkNotNull(backup);
+    public TileImageViewAdapter(Bitmap bitmap, BitmapRegionDecoder regionDecoder) {
+        mScreenNail = new BitmapScreenNail(Utils.checkNotNull(bitmap), 0);
         mRegionDecoder = regionDecoder;
         mImageWidth = regionDecoder.getWidth();
         mImageHeight = regionDecoder.getHeight();
@@ -49,7 +49,7 @@
     }
 
     public synchronized void clear() {
-        mBackupImage = null;
+        mScreenNail = null;
         mImageWidth = 0;
         mImageHeight = 0;
         mLevelCount = 0;
@@ -57,8 +57,18 @@
         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) {
+        mScreenNail = new BitmapScreenNail(Utils.checkNotNull(bitmap), 0);
+        mImageWidth = width;
+        mImageHeight = height;
+        mRegionDecoder = null;
+        mLevelCount = 0;
+        mFailedToLoad = false;
+    }
+
+    public synchronized void setScreenNail(
+            ScreenNail screenNail, int width, int height) {
+        mScreenNail = Utils.checkNotNull(screenNail);
         mImageWidth = width;
         mImageHeight = height;
         mRegionDecoder = null;
@@ -76,7 +86,7 @@
 
     private int calculateLevelCount() {
         return Math.max(0, Utils.ceilLog2(
-                (float) mImageWidth / mBackupImage.getWidth()));
+                (float) mImageWidth / mScreenNail.getWidth()));
     }
 
     @Override
@@ -122,8 +132,8 @@
     }
 
     @Override
-    public Bitmap getBackupImage() {
-        return mBackupImage;
+    public ScreenNail getScreenNail() {
+        return mScreenNail;
     }
 
     @Override