Merge "Fix a NPE found by Moneky." into jb-dev
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
index 4ee7403..f674c0b 100644
--- a/src/com/android/gallery3d/app/AbstractGalleryActivity.java
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -45,6 +45,7 @@
     private StateManager mStateManager;
     private GalleryActionBar mActionBar;
     private OrientationManager mOrientationManager;
+    private TransitionStore mTransitionStore = new TransitionStore();
     private boolean mDisableToggleStatusBar;
 
     private AlertDialog mAlertDialog = null;
@@ -260,4 +261,9 @@
             win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
         }
     }
+
+    @Override
+    public TransitionStore getTransitionStore() {
+        return mTransitionStore;
+    }
 }
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
index 443e2bd..233b398 100644
--- a/src/com/android/gallery3d/app/ActivityState.java
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -59,6 +59,7 @@
 
     private boolean mDestroyed = false;
     private boolean mPlugged = false;
+    boolean mIsFinishing = false;
 
     protected ActivityState() {
     }
@@ -173,6 +174,9 @@
             activity.registerReceiver(mPowerIntentReceiver, filter);
         }
         onResume();
+
+        // the transition store should be cleared after resume;
+        mActivity.getTransitionStore().clear();
     }
 
     // a subclass of ActivityState should override the method to resume itself
@@ -196,4 +200,8 @@
     boolean isDestroyed() {
         return mDestroyed;
     }
+
+    public boolean isFinishing() {
+        return mIsFinishing;
+    }
 }
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index df0124c..3033b93 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -50,6 +50,7 @@
 import com.android.gallery3d.ui.GLCanvas;
 import com.android.gallery3d.ui.GLRoot;
 import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PhotoFallbackEffect;
 import com.android.gallery3d.ui.RelativePosition;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SlotView;
@@ -70,6 +71,7 @@
     public static final String KEY_SET_CENTER = "set-center";
     public static final String KEY_AUTO_SELECT_ALL = "auto-select-all";
     public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu";
+    public static final String KEY_RESUME_ANIMATION = "resume_animation";
 
     private static final int REQUEST_SLIDESHOW = 1;
     private static final int REQUEST_PHOTO = 2;
@@ -110,6 +112,30 @@
     private boolean mInitialSynced = false;
     private RelativePosition mOpenCenter = new RelativePosition();
 
+    private PhotoFallbackEffect mResumeEffect;
+    private PhotoFallbackEffect.PositionProvider mPositionProvider =
+            new PhotoFallbackEffect.PositionProvider() {
+        @Override
+        public Rect getPosition(int index) {
+            Rect rect = mSlotView.getSlotRect(index);
+            Rect bounds = mSlotView.bounds();
+            rect.offset(bounds.left - mSlotView.getScrollX(),
+                    bounds.top - mSlotView.getScrollY());
+            return rect;
+        }
+
+        @Override
+        public int getItemIndex(Path path) {
+            int start = mSlotView.getVisibleStart();
+            int end = mSlotView.getVisibleEnd();
+            for (int i = start; i < end; ++i) {
+                MediaItem item = mAlbumDataAdapter.get(i);
+                if (item != null && item.getPath() == path) return i;
+            }
+            return -1;
+        }
+    };
+
     private final GLView mRootPane = new GLView() {
         private final float mMatrix[] = new float[16];
 
@@ -144,6 +170,16 @@
             canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
             canvas.multiplyMatrix(mMatrix, 0);
             super.render(canvas);
+
+            if (mResumeEffect != null) {
+                boolean more = mResumeEffect.draw(canvas);
+                if (!more) {
+                    mResumeEffect = null;
+                    mAlbumView.setSlotFilter(null);
+                } else {
+                    invalidate();
+                }
+            }
             canvas.restore();
         }
     };
@@ -336,6 +372,14 @@
     protected void onResume() {
         super.onResume();
         mIsActive = true;
+
+        mResumeEffect = mActivity.getTransitionStore().get(KEY_RESUME_ANIMATION);
+        if (mResumeEffect != null) {
+            mAlbumView.setSlotFilter(mResumeEffect);
+            mResumeEffect.setPositionProvider(mPositionProvider);
+            mResumeEffect.start();
+        }
+
         setContentPane(mRootPane);
 
         Path path = mMediaSet.getPath();
@@ -359,6 +403,9 @@
     protected void onPause() {
         super.onPause();
         mIsActive = false;
+
+        mAlbumView.setSlotFilter(null);
+
         mAlbumDataAdapter.pause();
         mAlbumView.pause();
         DetailsHelper.pause();
@@ -549,7 +596,6 @@
                 if (data == null) return;
                 mFocusIndex = data.getIntExtra(PhotoPage.KEY_RETURN_INDEX_HINT, 0);
                 mSlotView.makeSlotVisible(mFocusIndex);
-                mSlotView.startRestoringAnimation(mFocusIndex);
                 break;
             }
             case REQUEST_DO_ANIMATION: {
diff --git a/src/com/android/gallery3d/app/AppBridge.java b/src/com/android/gallery3d/app/AppBridge.java
index 90cbd0b..5d6b1ec 100644
--- a/src/com/android/gallery3d/app/AppBridge.java
+++ b/src/com/android/gallery3d/app/AppBridge.java
@@ -34,6 +34,7 @@
     //  These are requests sent from PhotoPage to the app
     //////////////////////////////////////////////////////////////////////////
 
+    public abstract boolean isPanorama();
     public abstract ScreenNail attachScreenNail();
     public abstract void detachScreenNail();
 
diff --git a/src/com/android/gallery3d/app/GalleryActivity.java b/src/com/android/gallery3d/app/GalleryActivity.java
index 0c6375f..33c77fb 100644
--- a/src/com/android/gallery3d/app/GalleryActivity.java
+++ b/src/com/android/gallery3d/app/GalleryActivity.java
@@ -23,4 +23,5 @@
     public GLRoot getGLRoot();
     public GalleryActionBar getGalleryActionBar();
     public OrientationManager getOrientationManager();
+    public TransitionStore getTransitionStore();
 }
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
index d88b72c..66b423f 100644
--- a/src/com/android/gallery3d/app/PhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -147,6 +147,7 @@
     private int mSize = 0;
     private Path mItemPath;
     private int mCameraIndex;
+    private boolean mIsPanorama;
     private boolean mIsActive;
     private boolean mNeedFullImage;
 
@@ -164,12 +165,14 @@
     // find the image being viewed. cameraIndex is the index of the camera
     // preview. If cameraIndex < 0, there is no camera preview.
     public PhotoDataAdapter(GalleryActivity activity, PhotoView view,
-            MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex) {
+            MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex,
+            boolean isPanorama) {
         mSource = Utils.checkNotNull(mediaSet);
         mPhotoView = Utils.checkNotNull(view);
         mItemPath = Utils.checkNotNull(itemPath);
         mCurrentIndex = indexHint;
         mCameraIndex = cameraIndex;
+        mIsPanorama = isPanorama;
         mThreadPool = activity.getThreadPool();
         mNeedFullImage = true;
 
@@ -429,6 +432,11 @@
     }
 
     @Override
+    public boolean isPanorama(int offset) {
+        return isCamera(offset) && mIsPanorama;
+    }
+
+    @Override
     public boolean isVideo(int offset) {
         MediaItem item = getItem(mCurrentIndex + offset);
         return (item == null)
@@ -469,8 +477,12 @@
         return mCurrentIndex;
     }
 
-    public MediaItem getCurrentMediaItem() {
-        return mData[mCurrentIndex % DATA_CACHE_SIZE];
+    public MediaItem getMediaItem(int offset) {
+        int index = mCurrentIndex + offset;
+        if (index >= mContentStart && index < mContentEnd) {
+            return mData[index % DATA_CACHE_SIZE];
+        }
+        return null;
     }
 
     public void setCurrentPhoto(Path path, int indexHint) {
@@ -482,7 +494,7 @@
         fireDataChange();
 
         // We need to reload content if the path doesn't match.
-        MediaItem item = getCurrentMediaItem();
+        MediaItem item = getMediaItem(0);
         if (item != null && item.getPath() != path) {
             if (mReloadTask != null) mReloadTask.notifyDirty();
         }
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index 493e4aa..1778f26 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -37,6 +37,7 @@
 import android.widget.Toast;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.data.DataManager;
 import com.android.gallery3d.data.MediaDetails;
 import com.android.gallery3d.data.MediaItem;
@@ -52,9 +53,12 @@
 import com.android.gallery3d.ui.DetailsHelper.CloseListener;
 import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
 import com.android.gallery3d.ui.GLCanvas;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
 import com.android.gallery3d.ui.GLView;
 import com.android.gallery3d.ui.ImportCompleteListener;
 import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.PhotoFallbackEffect;
 import com.android.gallery3d.ui.PhotoView;
 import com.android.gallery3d.ui.SelectionManager;
 import com.android.gallery3d.ui.SynchronizedHandler;
@@ -129,7 +133,6 @@
         public void resume();
         public void pause();
         public boolean isEmpty();
-        public MediaItem getCurrentMediaItem();
         public void setCurrentPhoto(Path path, int indexHint);
     }
 
@@ -213,7 +216,8 @@
             }
             PhotoDataAdapter pda = new PhotoDataAdapter(
                     mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
-                    mAppBridge == null ? -1 : 0);
+                    mAppBridge == null ? -1 : 0,
+                    mAppBridge == null ? false : mAppBridge.isPanorama());
             mModel = pda;
             mPhotoView.setModel(mModel);
 
@@ -223,7 +227,7 @@
                 public void onPhotoChanged(int index, Path item) {
                     mCurrentIndex = index;
                     if (item != null) {
-                        MediaItem photo = mModel.getCurrentMediaItem();
+                        MediaItem photo = mModel.getMediaItem(0);
                         if (photo != null) updateCurrentPhoto(photo);
                     }
                     updateBars();
@@ -232,7 +236,7 @@
                 @Override
                 public void onLoadingFinished() {
                     if (!mModel.isEmpty()) {
-                        MediaItem photo = mModel.getCurrentMediaItem();
+                        MediaItem photo = mModel.getMediaItem(0);
                         if (photo != null) updateCurrentPhoto(photo);
                     } else if (mIsActive) {
                         mActivity.getStateManager().finishState(PhotoPage.this);
@@ -519,7 +523,7 @@
 
     @Override
     protected boolean onItemSelected(MenuItem item) {
-        MediaItem current = mModel.getCurrentMediaItem();
+        MediaItem current = mModel.getMediaItem(0);
 
         if (current == null) {
             // item is not ready, ignore
@@ -624,7 +628,7 @@
             if (mAppBridge.onSingleTapUp(x, y)) return;
         }
 
-        MediaItem item = mModel.getCurrentMediaItem();
+        MediaItem item = mModel.getMediaItem(0);
         if (item == null || item == mScreenNailItem) {
             // item is not ready or it is camera preview, ignore
             return;
@@ -724,11 +728,49 @@
         }
     }
 
+    private class PreparePhotoFallback implements OnGLIdleListener {
+        private PhotoFallbackEffect mPhotoFallback = new PhotoFallbackEffect();
+        private boolean mResultReady = false;
+
+        public synchronized PhotoFallbackEffect get() {
+            while (!mResultReady) {
+                Utils.waitWithoutInterrupt(this);
+            }
+            return mPhotoFallback;
+        }
+
+        @Override
+        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+            mPhotoFallback = mPhotoView.buildFallbackEffect(mRootPane, canvas);
+            synchronized (this) {
+                mResultReady = true;
+                notifyAll();
+            }
+            return false;
+        }
+    }
+
+    private void preparePhotoFallbackView() {
+        GLRoot root = mActivity.getGLRoot();
+        PreparePhotoFallback task = new PreparePhotoFallback();
+        root.unlockRenderThread();
+        PhotoFallbackEffect anim;
+        try {
+            root.addOnGLIdleListener(task);
+            anim = task.get();
+        } finally {
+            root.lockRenderThread();
+        }
+        mActivity.getTransitionStore().put(
+                AlbumPage.KEY_RESUME_ANIMATION, anim);
+    }
+
     @Override
     public void onPause() {
         mActivity.getGLRoot().unfreeze();
         mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
         super.onPause();
+        if (isFinishing()) preparePhotoFallbackView();
         mIsActive = false;
 
         DetailsHelper.pause();
@@ -789,7 +831,7 @@
 
         @Override
         public MediaDetails getDetails() {
-            return mModel.getCurrentMediaItem().getDetails();
+            return mModel.getMediaItem(0).getDetails();
         }
 
         @Override
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
index 4ee6a01..f26f405 100644
--- a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
@@ -190,12 +190,17 @@
     }
 
     @Override
+    public boolean isPanorama(int offset) {
+        return false;
+    }
+
+    @Override
     public boolean isVideo(int offset) {
         return mItem.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
     }
 
-    public MediaItem getCurrentMediaItem() {
-        return mItem;
+    public MediaItem getMediaItem(int offset) {
+        return offset == 0 ? mItem : null;
     }
 
     public int getCurrentIndex() {
diff --git a/src/com/android/gallery3d/app/StateManager.java b/src/com/android/gallery3d/app/StateManager.java
index 096602a..02e9976 100644
--- a/src/com/android/gallery3d/app/StateManager.java
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -175,6 +175,7 @@
 
         // Remove the top state.
         mStack.pop();
+        state.mIsFinishing = true;
         if (mIsResumed) state.onPause();
         mContext.getGLRoot().setContentPane(null);
         state.onDestroy();
diff --git a/src/com/android/gallery3d/app/TransitionStore.java b/src/com/android/gallery3d/app/TransitionStore.java
new file mode 100644
index 0000000..9c09e7b
--- /dev/null
+++ b/src/com/android/gallery3d/app/TransitionStore.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import java.util.HashMap;
+
+public class TransitionStore {
+    private HashMap<Object, Object> mStorage = new HashMap<Object, Object>();
+
+    public void put(Object key, Object value) {
+        mStorage.put(key, value);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T get(Object key) {
+        return (T) mStorage.get(key);
+    }
+
+    public void clear() {
+        mStorage.clear();
+    }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
index 922e2c3..5f1e3a1 100644
--- a/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
+++ b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
@@ -24,10 +24,14 @@
 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";
+
+    public interface SlotFilter {
+        public boolean acceptSlot(int index);
+    }
+
+    private static final int PLACEHOLDER_COLOR = 0xFF222222;
     private static final int CACHE_SIZE = 96;
 
     private AlbumSlidingWindow mDataWindow;
@@ -41,6 +45,8 @@
     private Path mHighlightItemPath = null;
     private boolean mInSelectionMode;
 
+    private SlotFilter mSlotFilter;
+
     public AlbumSlotRenderer(GalleryActivity activity, SlotView slotView,
             SelectionManager selectionManager) {
         super((Context) activity);
@@ -92,6 +98,8 @@
 
     @Override
     public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+        if (mSlotFilter != null && !mSlotFilter.acceptSlot(index)) return 0;
+
         AlbumSlidingWindow.AlbumEntry entry = mDataWindow.get(index);
 
         int renderRequestFlags = 0;
@@ -183,4 +191,8 @@
     public void onSlotSizeChanged(int width, int height) {
         // Do nothing
     }
+
+    public void setSlotFilter(SlotFilter slotFilter) {
+        mSlotFilter = slotFilter;
+    }
 }
diff --git a/src/com/android/gallery3d/ui/PhotoFallbackEffect.java b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
new file mode 100644
index 0000000..cd930bd
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.AlbumSlotRenderer.SlotFilter;
+
+import java.util.ArrayList;
+
+public class PhotoFallbackEffect extends Animation implements SlotFilter {
+
+    private static final int ANIM_DURATION = 300;
+    private static final Interpolator ANIM_INTERPOLATE = new DecelerateInterpolator(1.5f);
+
+    public static class Entry {
+        public int index;
+        public Path path;
+        public Rect source;
+        public Rect dest;
+        public RawTexture texture;
+
+        public Entry(Path path, Rect source, RawTexture texture) {
+            this.path = path;
+            this.source = source;
+            this.texture = texture;
+        }
+    }
+
+    public interface PositionProvider {
+        public Rect getPosition(int index);
+        public int getItemIndex(Path path);
+    }
+
+    private RectF mSource = new RectF();
+    private RectF mTarget = new RectF();
+    private float mProgress;
+    private PositionProvider mPositionProvider;
+
+    private ArrayList<Entry> mList = new ArrayList<Entry>();
+
+    public PhotoFallbackEffect() {
+        setDuration(ANIM_DURATION);
+        setInterpolator(ANIM_INTERPOLATE);
+    }
+
+    public void addEntry(Path path, Rect rect, RawTexture texture) {
+        mList.add(new Entry(path, rect, texture));
+    }
+
+    public Entry getEntry(Path path) {
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.path == path) return entry;
+        }
+        return null;
+    }
+
+    public boolean draw(GLCanvas canvas) {
+        boolean more = calculate(AnimationTime.get());
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.index < 0) continue;
+            entry.dest = mPositionProvider.getPosition(entry.index);
+            drawEntry(canvas, entry);
+        }
+        return more;
+    }
+
+    private void drawEntry(GLCanvas canvas, Entry entry) {
+        if (!entry.texture.isLoaded(canvas)) return;
+
+        int w = entry.texture.getWidth();
+        int h = entry.texture.getHeight();
+
+        Rect s = entry.source;
+        Rect d = entry.dest;
+
+        // the following calculation is based on d.width() == d.height()
+
+        float p = mProgress;
+
+        float fullScale = (float) d.height() / Math.min(s.width(), s.height());
+        float scale = fullScale * p + 1 * (1 - p);
+
+        float cx = d.centerX() * p + s.centerX() * (1 - p);
+        float cy = d.centerY() * p + s.centerY() * (1 - p);
+
+        float ch = s.height() * scale;
+        float cw = s.width() * scale;
+
+        if (w > h) {
+            // draw the center part
+            mTarget.set(cx - ch / 2, cy - ch / 2, cx + ch / 2, cy + ch / 2);
+            mSource.set((w - h) / 2, 0, (w + h) / 2, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(1 - p);
+
+            // draw the left part
+            mTarget.set(cx - cw / 2, cy - ch / 2, cx - ch / 2, cy + ch / 2);
+            mSource.set(0, 0, (w - h) / 2, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            // draw the right part
+            mTarget.set(cx + ch / 2, cy - ch / 2, cx + cw / 2, cy + ch / 2);
+            mSource.set((w + h) / 2, 0, w, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.restore();
+        } else {
+            // draw the center part
+            mTarget.set(cx - cw / 2, cy - cw / 2, cx + cw / 2, cy + cw / 2);
+            mSource.set(0, (h - w) / 2, w, (h + w) / 2);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+            canvas.multiplyAlpha(1 - p);
+
+            // draw the upper part
+            mTarget.set(cx - cw / 2, cy - ch / 2, cx + cw / 2, cy - cw / 2);
+            mSource.set(0, 0, w, (h - w) / 2);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            // draw the bottom part
+            mTarget.set(cx - cw / 2, cy + cw / 2, cx + cw / 2, cy + ch / 2);
+            mSource.set(0, (w + h) / 2, w, h);
+            canvas.drawTexture(entry.texture, mSource, mTarget);
+
+            canvas.restore();
+        }
+    }
+
+    @Override
+    protected void onCalculate(float progress) {
+        mProgress = progress;
+    }
+
+    public void setPositionProvider(PositionProvider provider) {
+        mPositionProvider = provider;
+        if (mPositionProvider != null) {
+            for (int i = 0, n = mList.size(); i < n; ++i) {
+                Entry entry = mList.get(i);
+                entry.index = mPositionProvider.getItemIndex(entry.path);
+            }
+        }
+    }
+
+    @Override
+    public boolean acceptSlot(int index) {
+        for (int i = 0, n = mList.size(); i < n; ++i) {
+            Entry entry = mList.get(i);
+            if (entry.index == index) return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
index 7f3bee0..46d7c93 100644
--- a/src/com/android/gallery3d/ui/PhotoView.java
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -28,11 +28,10 @@
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryActivity;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.util.RangeArray;
 
-import java.util.Arrays;
-
 public class PhotoView extends GLView {
     @SuppressWarnings("unused")
     private static final String TAG = "PhotoView";
@@ -55,6 +54,9 @@
         // not avaiable, width = height = 0.
         public void getImageSize(int offset, Size size);
 
+        // Returns the media item for the specified picture.
+        public MediaItem getMediaItem(int offset);
+
         // Returns the rotation for the specified picture.
         public int getImageRotation(int offset);
 
@@ -69,6 +71,9 @@
         // Returns true if the item is the Camera preview.
         public boolean isCamera(int offset);
 
+        // Returns true if the item is the Panorama.
+        public boolean isPanorama(int offset);
+
         // Returns true if the item is a Video.
         public boolean isVideo(int offset);
     }
@@ -401,6 +406,10 @@
         return (mCompensation - mDisplayRotation + 360) % 360;
     }
 
+    private int getPanoramaRotation() {
+        return mCompensation;
+    }
+
     ////////////////////////////////////////////////////////////////////////////
     //  Pictures
     ////////////////////////////////////////////////////////////////////////////
@@ -416,6 +425,7 @@
     class FullPicture implements Picture {
         private int mRotation;
         private boolean mIsCamera;
+        private boolean mIsPanorama;
         private boolean mIsVideo;
         private boolean mWasCameraCenter;
 
@@ -429,6 +439,7 @@
             mTileView.notifyModelInvalidated();
 
             mIsCamera = mModel.isCamera(0);
+            mIsPanorama = mModel.isPanorama(0);
             mIsVideo = mModel.isVideo(0);
             setScreenNail(mModel.getScreenNail(0));
             updateSize(false);
@@ -437,7 +448,9 @@
 
         @Override
         public void updateSize(boolean force) {
-            if (mIsCamera) {
+            if (mIsPanorama) {
+                mRotation = getPanoramaRotation();
+            } else if (mIsCamera) {
                 mRotation = getCameraRotation();
             } else {
                 mRotation = mModel.getImageRotation(0);
@@ -627,6 +640,7 @@
         private ScreenNail mScreenNail;
         private Size mSize = new Size();
         private boolean mIsCamera;
+        private boolean mIsPanorama;
         private boolean mIsVideo;
 
         public ScreenNailPicture(int index) {
@@ -636,6 +650,7 @@
         @Override
         public void reload() {
             mIsCamera = mModel.isCamera(mIndex);
+            mIsPanorama = mModel.isPanorama(mIndex);
             mIsVideo = mModel.isVideo(mIndex);
             setScreenNail(mModel.getScreenNail(mIndex));
         }
@@ -701,7 +716,9 @@
 
         @Override
         public void updateSize(boolean force) {
-            if (mIsCamera) {
+            if (mIsPanorama) {
+                mRotation = getPanoramaRotation();
+            } else if (mIsCamera) {
                 mRotation = getCameraRotation();
             } else {
                 mRotation = mModel.getImageRotation(mIndex);
@@ -1337,4 +1354,33 @@
     public void setListener(Listener listener) {
         mListener = listener;
     }
+
+    public Rect getPhotoRect(int index) {
+        return mPositionController.getPosition(index);
+    }
+
+
+    public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
+        Rect location = new Rect();
+        Utils.assertTrue(root.getBoundsOf(this, location));
+
+        Rect fullRect = bounds();
+        PhotoFallbackEffect effect = new PhotoFallbackEffect();
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            MediaItem item = mModel.getMediaItem(i);
+            if (item == null) continue;
+            ScreenNail sc = mModel.getScreenNail(i);
+            if (sc == null) continue;
+            Rect rect = new Rect(getPhotoRect(i));
+            if (!Rect.intersects(fullRect, rect)) continue;
+            rect.offset(location.left, location.top);
+
+            RawTexture texture = new RawTexture(sc.getWidth(), sc.getHeight(), true);
+            canvas.beginRenderTarget(texture);
+            sc.draw(canvas, 0, 0, sc.getWidth(), sc.getHeight());
+            canvas.endRenderTarget();
+            effect.addEntry(item.getPath(), rect, texture);
+        }
+        return effect;
+    }
 }
diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java
index 0334c55..88a2e0d 100644
--- a/src/com/android/gallery3d/ui/SlotView.java
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -179,12 +179,6 @@
         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) {
         if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return;
         if (WIDE) {
@@ -378,22 +372,6 @@
         }
     }
 
-    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));
-            }
-        }
-    }
-
     // This Spec class is used to specify the size of each slot in the SlotView.
     // There are two ways to do it:
     //