am 9025dba3: (-s ours) (DO NOT MERGE)

* commit '9025dba35881391c24bdaa88d1ec84d15d722f65':
  (DO NOT MERGE)
diff --git a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
index 060d7f3..c34e896 100644
--- a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
+++ b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java
@@ -155,6 +155,17 @@
         return resizeBitmapByScale(bitmap, scale, recycle);
     }
 
+    // Resize the bitmap if each side is >= targetSize * 2
+    public static Bitmap resizeDownIfTooBig(
+            Bitmap bitmap, int targetSize, boolean recycle) {
+        int srcWidth = bitmap.getWidth();
+        int srcHeight = bitmap.getHeight();
+        float scale = Math.max(
+                (float) targetSize / srcWidth, (float) targetSize / srcHeight);
+        if (scale > 0.5f) return bitmap;
+        return resizeBitmapByScale(bitmap, scale, recycle);
+    }
+
     // Crops a square from the center of the original image.
     public static Bitmap cropCenter(Bitmap bitmap, boolean recycle) {
         int width = bitmap.getWidth();
diff --git a/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
index d652ac9..46de03f 100644
--- a/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
+++ b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java
@@ -29,14 +29,14 @@
     @SuppressWarnings("unused")
     private static final String TAG = "EntrySchema";
 
-    private static final int TYPE_STRING = 0;
-    private static final int TYPE_BOOLEAN = 1;
-    private static final int TYPE_SHORT = 2;
-    private static final int TYPE_INT = 3;
-    private static final int TYPE_LONG = 4;
-    private static final int TYPE_FLOAT = 5;
-    private static final int TYPE_DOUBLE = 6;
-    private static final int TYPE_BLOB = 7;
+    public static final int TYPE_STRING = 0;
+    public static final int TYPE_BOOLEAN = 1;
+    public static final int TYPE_SHORT = 2;
+    public static final int TYPE_INT = 3;
+    public static final int TYPE_LONG = 4;
+    public static final int TYPE_FLOAT = 5;
+    public static final int TYPE_DOUBLE = 6;
+    public static final int TYPE_BLOB = 7;
     private static final String SQLITE_TYPES[] = {
             "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" };
 
@@ -91,7 +91,7 @@
         return -1;
     }
 
-    private ColumnInfo getColumn(String columnName) {
+    public ColumnInfo getColumn(String columnName) {
         int index = getColumnIndex(columnName);
         return (index < 0) ? null : mColumnInfo[index];
     }
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index 672c0da..55feb38 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -148,6 +148,18 @@
         }
     }
 
+    private void onDown(int index) {
+        MediaItem item = mAlbumDataAdapter.get(index);
+        Path path = (item == null) ? null : item.getPath();
+        mSelectionManager.setPressedPath(path);
+        mAlbumView.invalidate();
+    }
+
+    private void onUp() {
+        mSelectionManager.setPressedPath(null);
+        mAlbumView.invalidate();
+    }
+
     public void onSingleTapUp(int slotIndex) {
         MediaItem item = mAlbumDataAdapter.get(slotIndex);
         if (item == null) {
@@ -357,15 +369,26 @@
         mSelectionManager.setSelectionListener(this);
         mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager);
         Config.AlbumPage config = Config.AlbumPage.get((Context) mActivity);
-        mAlbumView = new AlbumView(mActivity,
-                config.slotWidth, config.slotHeight, config.displayItemSize);
+        mAlbumView = new AlbumView(mActivity, config.slotViewSpec,
+                0 /* don't cache thumbnail */);
         mAlbumView.setSelectionDrawer(mGridDrawer);
         mRootPane.addComponent(mAlbumView);
         mAlbumView.setListener(new SlotView.SimpleListener() {
             @Override
+            public void onDown(int index) {
+                AlbumPage.this.onDown(index);
+            }
+
+            @Override
+            public void onUp() {
+                AlbumPage.this.onUp();
+            }
+
+            @Override
             public void onSingleTapUp(int slotIndex) {
                 AlbumPage.this.onSingleTapUp(slotIndex);
             }
+
             @Override
             public void onLongTap(int slotIndex) {
                 AlbumPage.this.onLongTap(slotIndex);
@@ -395,7 +418,8 @@
     private void showDetails() {
         mShowDetails = true;
         if (mDetailsHelper == null) {
-            mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext());
+            mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext(),
+                    mSelectionManager);
             mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
             mDetailsHelper.setCloseListener(new CloseListener() {
                 public void onClose() {
@@ -521,10 +545,6 @@
                 break;
             }
             case SelectionManager.SELECT_ALL_MODE: {
-                int count = mSelectionManager.getSelectedCount();
-                String format = mActivity.getResources().getQuantityString(
-                        R.plurals.number_of_items_selected, count);
-                mActionModeHandler.setTitle(String.format(format, count));
                 mActionModeHandler.updateSupportedOperation();
                 mRootPane.invalidate();
                 break;
diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java
index b86aee8..ef5847e 100644
--- a/src/com/android/gallery3d/app/AlbumPicker.java
+++ b/src/com/android/gallery3d/app/AlbumPicker.java
@@ -18,26 +18,17 @@
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.ui.GLRoot;
-import com.android.gallery3d.ui.GLRootView;
 
 import android.content.Intent;
 import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
 
-public class AlbumPicker extends AbstractGalleryActivity
-        implements OnClickListener {
+public class AlbumPicker extends PickerActivity {
 
     public static final String KEY_ALBUM_PATH = "album-path";
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.dialog_picker);
-        ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
-        findViewById(R.id.cancel).setOnClickListener(this);
         setTitle(R.string.select_album);
         Intent intent = getIntent();
         Bundle extras = intent.getExtras();
@@ -48,21 +39,4 @@
                 getDataManager().getTopSetPath(DataManager.INCLUDE_IMAGE));
         getStateManager().startState(AlbumSetPage.class, data);
     }
-
-    @Override
-    public void onBackPressed() {
-        // send the back event to the top sub-state
-        GLRoot root = getGLRoot();
-        root.lockRenderThread();
-        try {
-            getStateManager().getTopState().onBackPressed();
-        } finally {
-            root.unlockRenderThread();
-        }
-    }
-
-    @Override
-    public void onClick(View v) {
-        if (v.getId() == R.id.cancel) finish();
-    }
 }
diff --git a/src/com/android/gallery3d/app/AlbumSetDataAdapter.java b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java
index 85f3bf5..8322d2b 100644
--- a/src/com/android/gallery3d/app/AlbumSetDataAdapter.java
+++ b/src/com/android/gallery3d/app/AlbumSetDataAdapter.java
@@ -41,7 +41,7 @@
     private static final int INDEX_NONE = -1;
 
     private static final int MIN_LOAD_COUNT = 4;
-    private static final int MAX_COVER_COUNT = 4;
+    private static final int MAX_COVER_COUNT = 1;
 
     private static final int MSG_LOAD_START = 1;
     private static final int MSG_LOAD_FINISH = 2;
diff --git a/src/com/android/gallery3d/app/AlbumSetPage.java b/src/com/android/gallery3d/app/AlbumSetPage.java
index 0970572..0726ba1 100644
--- a/src/com/android/gallery3d/app/AlbumSetPage.java
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -63,7 +63,7 @@
     public static final String KEY_MEDIA_PATH = "media-path";
     public static final String KEY_SET_TITLE = "set-title";
     public static final String KEY_SET_SUBTITLE = "set-subtitle";
-    public static final String KEY_SELECTED_TAB_TYPE = "selected-tab";
+    public static final String KEY_SELECTED_CLUSTER_TYPE = "selected-cluster";
 
     private static final int DATA_CACHE_SIZE = 256;
     private static final int REQUEST_DO_ANIMATION = 1;
@@ -76,7 +76,8 @@
     private MediaSet mMediaSet;
     private String mTitle;
     private String mSubtitle;
-    private boolean mShowClusterTabs;
+    private boolean mShowClusterMenu;
+    private int mSelectedAction;
 
     protected SelectionManager mSelectionManager;
     private AlbumSetDataAdapter mAlbumSetDataAdapter;
@@ -209,6 +210,18 @@
         }
     }
 
+    private void onDown(int index) {
+        MediaSet set = mAlbumSetDataAdapter.getMediaSet(index);
+        Path path = (set == null) ? null : set.getPath();
+        mSelectionManager.setPressedPath(path);
+        mAlbumSetView.invalidate();
+    }
+
+    private void onUp() {
+        mSelectionManager.setPressedPath(null);
+        mAlbumSetView.invalidate();
+    }
+
     public void onLongTap(int slotIndex) {
         if (mGetContent || mGetAlbum) return;
         if (mShowDetails) {
@@ -229,7 +242,7 @@
         String newPath = FilterUtils.switchClusterPath(basePath, clusterType);
         Bundle data = new Bundle(getData());
         data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
-        data.putInt(KEY_SELECTED_TAB_TYPE, clusterType);
+        data.putInt(KEY_SELECTED_CLUSTER_TYPE, clusterType);
         mAlbumSetView.savePositions(PositionRepository.getInstance(mActivity));
         mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
     }
@@ -260,8 +273,8 @@
         mDetailsSource = new MyDetailsSource();
         GalleryActionBar actionBar = mActivity.getGalleryActionBar();
         if (actionBar != null) {
-            actionBar.setSelectedTab(data.getInt(
-                    AlbumSetPage.KEY_SELECTED_TAB_TYPE, FilterUtils.CLUSTER_BY_ALBUM));
+            mSelectedAction = data.getInt(
+                    AlbumSetPage.KEY_SELECTED_CLUSTER_TYPE, FilterUtils.CLUSTER_BY_ALBUM);
         }
         startTransition();
     }
@@ -277,7 +290,7 @@
         mEyePosition.pause();
         DetailsHelper.pause();
         GalleryActionBar actionBar = mActivity.getGalleryActionBar();
-        if (actionBar != null) actionBar.hideClusterTabs();
+        if (actionBar != null) actionBar.hideClusterMenu();
     }
 
     @Override
@@ -291,7 +304,7 @@
         mEyePosition.resume();
         mActionModeHandler.resume();
         GalleryActionBar actionBar = mActivity.getGalleryActionBar();
-        if (mShowClusterTabs && actionBar != null) actionBar.showClusterTabs(this);
+        if (mShowClusterMenu && actionBar != null) actionBar.showClusterMenu(mSelectedAction, this);
     }
 
     private void initializeData(Bundle data) {
@@ -313,14 +326,23 @@
         mGridDrawer = new GridDrawer((Context) mActivity, mSelectionManager);
         Config.AlbumSetPage config = Config.AlbumSetPage.get((Context) mActivity);
         mAlbumSetView = new AlbumSetView(mActivity, mGridDrawer,
-                config.slotWidth, config.slotHeight,
-                config.displayItemSize, config.labelFontSize,
-                config.labelOffsetY, config.labelMargin);
+                config.slotViewSpec, config.labelSpec);
         mAlbumSetView.setListener(new SlotView.SimpleListener() {
             @Override
+            public void onDown(int index) {
+                AlbumSetPage.this.onDown(index);
+            }
+
+            @Override
+            public void onUp() {
+                AlbumSetPage.this.onUp();
+            }
+
+            @Override
             public void onSingleTapUp(int slotIndex) {
                 AlbumSetPage.this.onSingleTapUp(slotIndex);
             }
+
             @Override
             public void onLongTap(int slotIndex) {
                 AlbumSetPage.this.onLongTap(slotIndex);
@@ -363,7 +385,7 @@
             inflater.inflate(R.menu.pickup, menu);
             actionBar.setTitle(R.string.select_album);
         } else {
-            mShowClusterTabs = !inAlbum;
+            mShowClusterMenu = !inAlbum;
             inflater.inflate(R.menu.albumset, menu);
             if (mTitle != null) {
                 actionBar.setTitle(mTitle);
@@ -485,13 +507,13 @@
 
         switch (mode) {
             case SelectionManager.ENTER_SELECTION_MODE: {
-                mActivity.getGalleryActionBar().hideClusterTabs();
+                mActivity.getGalleryActionBar().hideClusterMenu();
                 mActionMode = mActionModeHandler.startActionMode();
                 break;
             }
             case SelectionManager.LEAVE_SELECTION_MODE: {
                 mActionMode.finish();
-                mActivity.getGalleryActionBar().showClusterTabs(this);
+                mActivity.getGalleryActionBar().showClusterMenu(mSelectedAction, this);
                 mRootPane.invalidate();
                 break;
             }
@@ -519,7 +541,8 @@
     private void showDetails() {
         mShowDetails = true;
         if (mDetailsHelper == null) {
-            mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext());
+            mHighlightDrawer = new HighlightDrawer(mActivity.getAndroidContext(),
+                    mSelectionManager);
             mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
             mDetailsHelper.setCloseListener(new CloseListener() {
                 public void onClose() {
diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java
index 4586235..914ea55 100644
--- a/src/com/android/gallery3d/app/Config.java
+++ b/src/com/android/gallery3d/app/Config.java
@@ -17,6 +17,8 @@
 package com.android.gallery3d.app;
 
 import com.android.gallery3d.R;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.AlbumSetView;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -25,12 +27,8 @@
     public static class AlbumSetPage {
         private static AlbumSetPage sInstance;
 
-        public final int slotWidth;
-        public final int slotHeight;
-        public final int displayItemSize;
-        public final int labelFontSize;
-        public final int labelOffsetY;
-        public final int labelMargin;
+        public SlotView.Spec slotViewSpec;
+        public AlbumSetView.LabelSpec labelSpec;
 
         public static synchronized AlbumSetPage get(Context context) {
             if (sInstance == null) {
@@ -41,21 +39,34 @@
 
         private AlbumSetPage(Context context) {
             Resources r = context.getResources();
-            slotWidth = r.getDimensionPixelSize(R.dimen.albumset_slot_width);
-            slotHeight = r.getDimensionPixelSize(R.dimen.albumset_slot_height);
-            displayItemSize = r.getDimensionPixelSize(R.dimen.albumset_display_item_size);
-            labelFontSize = r.getDimensionPixelSize(R.dimen.albumset_label_font_size);
-            labelOffsetY = r.getDimensionPixelSize(R.dimen.albumset_label_offset_y);
-            labelMargin = r.getDimensionPixelSize(R.dimen.albumset_label_margin);
+
+            slotViewSpec = new SlotView.Spec();
+            slotViewSpec.rowsLand = r.getInteger(R.integer.albumset_rows_land);
+            slotViewSpec.rowsPort = r.getInteger(R.integer.albumset_rows_port);
+            slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.albumset_slot_gap);
+
+            labelSpec = new AlbumSetView.LabelSpec();
+            labelSpec.labelBackgroundHeight = r.getDimensionPixelSize(
+                    R.dimen.albumset_label_background_height);
+            labelSpec.titleOffset = r.getDimensionPixelSize(
+                    R.dimen.albumset_title_offset);
+            labelSpec.countOffset = r.getDimensionPixelSize(
+                    R.dimen.albumset_count_offset);
+            labelSpec.titleFontSize = r.getDimensionPixelSize(
+                    R.dimen.albumset_title_font_size);
+            labelSpec.countFontSize = r.getDimensionPixelSize(
+                    R.dimen.albumset_count_font_size);
+            labelSpec.leftMargin = r.getDimensionPixelSize(
+                    R.dimen.albumset_left_margin);
+            labelSpec.iconSize = r.getDimensionPixelSize(
+                    R.dimen.albumset_icon_size);
         }
     }
 
     public static class AlbumPage {
         private static AlbumPage sInstance;
 
-        public final int slotWidth;
-        public final int slotHeight;
-        public final int displayItemSize;
+        public SlotView.Spec slotViewSpec;
 
         public static synchronized AlbumPage get(Context context) {
             if (sInstance == null) {
@@ -66,20 +77,19 @@
 
         private AlbumPage(Context context) {
             Resources r = context.getResources();
-            slotWidth = r.getDimensionPixelSize(R.dimen.album_slot_width);
-            slotHeight = r.getDimensionPixelSize(R.dimen.album_slot_height);
-            displayItemSize = r.getDimensionPixelSize(R.dimen.album_display_item_size);
+
+            slotViewSpec = new SlotView.Spec();
+            slotViewSpec.rowsLand = r.getInteger(R.integer.album_rows_land);
+            slotViewSpec.rowsPort = r.getInteger(R.integer.album_rows_port);
+            slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.album_slot_gap);
         }
     }
 
     public static class ManageCachePage extends AlbumSetPage {
         private static ManageCachePage sInstance;
 
-        public final int cacheBarHeight;
-        public final int cacheBarPinLeftMargin;
-        public final int cacheBarPinRightMargin;
-        public final int cacheBarButtonRightMargin;
-        public final int cacheBarFontSize;
+        public final int cachePinSize;
+        public final int cachePinMargin;
 
         public static synchronized ManageCachePage get(Context context) {
             if (sInstance == null) {
@@ -91,13 +101,8 @@
         public ManageCachePage(Context context) {
             super(context);
             Resources r = context.getResources();
-            cacheBarHeight = r.getDimensionPixelSize(R.dimen.cache_bar_height);
-            cacheBarPinLeftMargin = r.getDimensionPixelSize(R.dimen.cache_bar_pin_left_margin);
-            cacheBarPinRightMargin = r.getDimensionPixelSize(
-                    R.dimen.cache_bar_pin_right_margin);
-            cacheBarButtonRightMargin = r.getDimensionPixelSize(
-                    R.dimen.cache_bar_button_right_margin);
-            cacheBarFontSize = r.getDimensionPixelSize(R.dimen.cache_bar_font_size);
+            cachePinSize = r.getDimensionPixelSize(R.dimen.cache_pin_size);
+            cachePinMargin = r.getDimensionPixelSize(R.dimen.cache_pin_margin);
         }
     }
 
diff --git a/src/com/android/gallery3d/app/CropImage.java b/src/com/android/gallery3d/app/CropImage.java
index 47c007b..03cd48f 100644
--- a/src/com/android/gallery3d/app/CropImage.java
+++ b/src/com/android/gallery3d/app/CropImage.java
@@ -16,28 +16,6 @@
 
 package com.android.gallery3d.app;
 
-import com.android.gallery3d.R;
-import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.common.Utils;
-import com.android.gallery3d.data.DataManager;
-import com.android.gallery3d.data.LocalImage;
-import com.android.gallery3d.data.MediaItem;
-import com.android.gallery3d.data.MediaObject;
-import com.android.gallery3d.data.Path;
-import com.android.gallery3d.picasasource.PicasaSource;
-import com.android.gallery3d.ui.BitmapTileProvider;
-import com.android.gallery3d.ui.CropView;
-import com.android.gallery3d.ui.GLRoot;
-import com.android.gallery3d.ui.SynchronizedHandler;
-import com.android.gallery3d.ui.TileImageViewAdapter;
-import com.android.gallery3d.util.Future;
-import com.android.gallery3d.util.FutureListener;
-import com.android.gallery3d.util.GalleryUtils;
-import com.android.gallery3d.util.InterruptableOutputStream;
-import com.android.gallery3d.util.ThreadPool.CancelListener;
-import com.android.gallery3d.util.ThreadPool.Job;
-import com.android.gallery3d.util.ThreadPool.JobContext;
-
 import android.app.ProgressDialog;
 import android.app.WallpaperManager;
 import android.content.ContentValues;
@@ -64,6 +42,28 @@
 import android.view.Window;
 import android.widget.Toast;
 
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalImage;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.BitmapTileProvider;
+import com.android.gallery3d.ui.CropView;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.InterruptableOutputStream;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -85,6 +85,7 @@
     private static final int MSG_LARGE_BITMAP = 1;
     private static final int MSG_BITMAP = 2;
     private static final int MSG_SAVE_COMPLETE = 3;
+    private static final int MSG_SHOW_SAVE_ERROR = 4;
 
     private static final int MAX_BACKUP_IMAGE_SIZE = 320;
     private static final int DEFAULT_COMPRESS_QUALITY = 90;
@@ -166,6 +167,14 @@
                         onBitmapAvailable((Bitmap) message.obj);
                         break;
                     }
+                    case MSG_SHOW_SAVE_ERROR: {
+                        mProgressDialog.dismiss();
+                        setResult(RESULT_CANCELED);
+                        Toast.makeText(CropImage.this,
+                                CropImage.this.getString(R.string.save_error),
+                                Toast.LENGTH_LONG).show();
+                        finish();
+                    }
                     case MSG_SAVE_COMPLETE: {
                         mProgressDialog.dismiss();
                         setResult(RESULT_OK, (Intent) message.obj);
@@ -418,12 +427,11 @@
             });
         try {
             bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
-            if (!jc.isCancelled()) return false;
+            return !jc.isCancelled();
         } finally {
             jc.setCancelListener(null);
             Utils.closeSilently(os);
         }
-        return false;
     }
 
     private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) {
@@ -469,9 +477,14 @@
                 new FutureListener<Intent>() {
             public void onFutureDone(Future<Intent> future) {
                 mSaveTask = null;
-                if (future.get() == null) return;
-                mMainHandler.sendMessage(mMainHandler.obtainMessage(
-                        MSG_SAVE_COMPLETE, future.get()));
+                if (future.isCancelled()) return;
+                Intent intent = future.get();
+                if (intent != null) {
+                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
+                            MSG_SAVE_COMPLETE, intent));
+                } else {
+                    mMainHandler.sendEmptyMessage(MSG_SHOW_SAVE_ERROR);
+                }
             }
         });
     }
diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java
index ebfc521..8a57824 100644
--- a/src/com/android/gallery3d/app/DialogPicker.java
+++ b/src/com/android/gallery3d/app/DialogPicker.java
@@ -17,26 +17,17 @@
 package com.android.gallery3d.app;
 
 import com.android.gallery3d.R;
-import com.android.gallery3d.ui.GLRoot;
-import com.android.gallery3d.ui.GLRootView;
 import com.android.gallery3d.util.GalleryUtils;
 
 import android.content.Intent;
 import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
 
-public class DialogPicker extends AbstractGalleryActivity
-        implements OnClickListener {
+public class DialogPicker extends PickerActivity {
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        setContentView(R.layout.dialog_picker);
-        ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
-        findViewById(R.id.cancel).setOnClickListener(this);
-
         int typeBits = GalleryUtils.determineTypeBits(this, getIntent());
         setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
         Intent intent = getIntent();
@@ -48,21 +39,4 @@
                 getDataManager().getTopSetPath(typeBits));
         getStateManager().startState(AlbumSetPage.class, data);
     }
-
-    @Override
-    public void onBackPressed() {
-        // send the back event to the top sub-state
-        GLRoot root = getGLRoot();
-        root.lockRenderThread();
-        try {
-            getStateManager().getTopState().onBackPressed();
-        } finally {
-            root.unlockRenderThread();
-        }
-    }
-
-    @Override
-    public void onClick(View v) {
-        if (v.getId() == R.id.cancel) finish();
-    }
 }
diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java
index 3df5e91..98a35a1 100644
--- a/src/com/android/gallery3d/app/GalleryActionBar.java
+++ b/src/com/android/gallery3d/app/GalleryActionBar.java
@@ -19,19 +19,22 @@
 import com.android.gallery3d.R;
 
 import android.app.ActionBar;
-import android.app.ActionBar.Tab;
 import android.app.Activity;
 import android.app.AlertDialog;
-import android.app.FragmentTransaction;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
 import android.widget.ShareActionProvider;
+import android.widget.TextView;
 
 import java.util.ArrayList;
 
-public class GalleryActionBar implements ActionBar.TabListener {
+public class GalleryActionBar implements ActionBar.OnNavigationListener {
     private static final String TAG = "GalleryActionBar";
 
     public interface ClusterRunner {
@@ -42,7 +45,7 @@
         public int action;
         public boolean enabled;
         public boolean visible;
-        public int tabTitle;
+        public int spinnerTitle;
         public int dialogTitle;
         public int clusterBy;
 
@@ -51,11 +54,11 @@
             this(action, applied, enabled, title, title, clusterBy);
         }
 
-        public ActionItem(int action, boolean applied, boolean enabled, int tabTitle,
+        public ActionItem(int action, boolean applied, boolean enabled, int spinnerTitle,
                 int dialogTitle, int clusterBy) {
             this.action = action;
             this.enabled = enabled;
-            this.tabTitle = tabTitle;
+            this.spinnerTitle = spinnerTitle;
             this.dialogTitle = dialogTitle;
             this.clusterBy = clusterBy;
             this.visible = true;
@@ -75,23 +78,47 @@
                 R.string.group_by_tags)
     };
 
+    private class ClusterAdapter extends BaseAdapter {
+
+        public int getCount() {
+            return sClusterItems.length;
+        }
+
+        public Object getItem(int position) {
+            return sClusterItems[position];
+        }
+
+        public long getItemId(int position) {
+            return sClusterItems[position].action;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.action_bar_text,
+                        parent, false);
+            }
+            TextView view = (TextView) convertView;
+            view.setText(sClusterItems[position].spinnerTitle);
+            return convertView;
+        }
+    }
+
     private ClusterRunner mClusterRunner;
     private CharSequence[] mTitles;
     private ArrayList<Integer> mActions;
     private Context mContext;
+    private LayoutInflater mInflater;
+    private GalleryActivity mActivity;
     private ActionBar mActionBar;
-    // We need this because ActionBar.getSelectedTab() doesn't work when
-    // ActionBar is hidden.
-    private Tab mCurrentTab;
+    private int mCurrentIndex;
+    private ClusterAdapter mAdapter = new ClusterAdapter();
 
-    public GalleryActionBar(Activity activity) {
-        mActionBar = activity.getActionBar();
-        mContext = activity;
-
-        for (ActionItem item : sClusterItems) {
-            mActionBar.addTab(mActionBar.newTab().setText(item.tabTitle).
-                    setTag(item).setTabListener(this));
-        }
+    public GalleryActionBar(GalleryActivity activity) {
+        mActionBar = ((Activity) activity).getActionBar();
+        mContext = activity.getAndroidContext();
+        mActivity = activity;
+        mInflater = ((Activity) mActivity).getLayoutInflater();
+        mCurrentIndex = 0;
     }
 
     public static int getHeight(Activity activity) {
@@ -131,12 +158,7 @@
     }
 
     public int getClusterTypeAction() {
-        if (mCurrentTab != null) {
-            ActionItem item = (ActionItem) mCurrentTab.getTag();
-            return item.action;
-        }
-        // By default, it's group-by-album
-        return FilterUtils.CLUSTER_BY_ALBUM;
+        return sClusterItems[mCurrentIndex].action;
     }
 
     public static String getClusterByTypeString(Context context, int type) {
@@ -157,19 +179,19 @@
         return shareActionProvider;
     }
 
-    public void showClusterTabs(ClusterRunner runner) {
-        Log.v(TAG, "showClusterTabs: runner=" + runner);
-        // setNavigationMode will trigger onTabSelected, so we should avoid
-        // triggering any callback here
+    public void showClusterMenu(int action, ClusterRunner runner) {
+        Log.v(TAG, "showClusterMenu: runner=" + runner);
+        // Don't set cluster runner until action bar is ready.
         mClusterRunner = null;
-        mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+        mActionBar.setListNavigationCallbacks(mAdapter, this);
+        mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+        setSelectedAction(action);
         mClusterRunner = runner;
     }
 
-    public void hideClusterTabs() {
+    public void hideClusterMenu() {
         mClusterRunner = null;
         mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
-        Log.v(TAG, "hideClusterTabs: runner=" + mClusterRunner);
     }
 
     public void showClusterDialog(final ClusterRunner clusterRunner) {
@@ -203,31 +225,28 @@
         return mActionBar == null ? 0 : mActionBar.getHeight();
     }
 
-    @Override
-    public void onTabSelected(Tab tab, FragmentTransaction ft) {
-        if (mCurrentTab == tab) return;
-        mCurrentTab = tab;
-        ActionItem item = (ActionItem) tab.getTag();
-        Log.v(TAG, "onTabSelected: clusterrRunner=" + mClusterRunner);
-        if (mClusterRunner != null) mClusterRunner.doCluster(item.action);
-    }
-
-    @Override
-    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
-    }
-
-    @Override
-    public void onTabReselected(Tab tab, FragmentTransaction ft) {
-    }
-
-    public boolean setSelectedTab(int type) {
-        for (int i = 0, n = sClusterItems.length; i < n; ++i) {
+    public boolean setSelectedAction(int type) {
+        for (int i = 0, n = sClusterItems.length; i < n; i++) {
             ActionItem item = sClusterItems[i];
             if (item.visible && item.action == type) {
-                mActionBar.selectTab(mActionBar.getTabAt(i));
+                mActionBar.setSelectedNavigationItem(i);
+                mCurrentIndex = i;
                 return true;
             }
         }
         return false;
     }
+
+    @Override
+    public boolean onNavigationItemSelected(int itemPosition, long itemId) {
+        if (itemPosition != mCurrentIndex && mClusterRunner != null) {
+            mActivity.getGLRoot().lockRenderThread();
+            try {
+                mClusterRunner.doCluster(sClusterItems[itemPosition].action);
+            } finally {
+                mActivity.getGLRoot().unlockRenderThread();
+            }
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/gallery3d/app/ManageCachePage.java b/src/com/android/gallery3d/app/ManageCachePage.java
index 940f1be..27f92e4 100644
--- a/src/com/android/gallery3d/app/ManageCachePage.java
+++ b/src/com/android/gallery3d/app/ManageCachePage.java
@@ -113,10 +113,10 @@
             int slotViewTop = GalleryActionBar.getHeight(activity);
             int slotViewBottom = bottom - top;
 
-            View cacheBar = activity.findViewById(R.id.cache_bar);
-            if (cacheBar != null) {
+            View footer = activity.findViewById(R.id.footer);
+            if (footer != null) {
                 int location[] = {0, 0};
-                cacheBar.getLocationOnScreen(location);
+                footer.getLocationOnScreen(location);
                 slotViewBottom = location[1];
             }
 
@@ -143,6 +143,18 @@
         mRootPane.invalidate();
     }
 
+    private void onDown(int index) {
+        MediaSet set = mAlbumSetDataAdapter.getMediaSet(index);
+        Path path = (set == null) ? null : set.getPath();
+        mSelectionManager.setPressedPath(path);
+        mAlbumSetView.invalidate();
+    }
+
+    private void onUp() {
+        mSelectionManager.setPressedPath(null);
+        mAlbumSetView.invalidate();
+    }
+
     public void onSingleTapUp(int slotIndex) {
         MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
         if (targetSet == null) return; // Content is dirty, we shall reload soon
@@ -279,15 +291,23 @@
         mStaticBackground = new StaticBackground(activity);
         mRootPane.addComponent(mStaticBackground);
 
-        mSelectionDrawer = new ManageCacheDrawer(
-                (Context) mActivity, mSelectionManager);
         Config.ManageCachePage config = Config.ManageCachePage.get(activity);
+        mSelectionDrawer = new ManageCacheDrawer((Context) mActivity,
+                mSelectionManager, config.cachePinSize, config.cachePinMargin);
         mAlbumSetView = new AlbumSetView(mActivity, mSelectionDrawer,
-                config.slotWidth, config.slotHeight,
-                config.displayItemSize, config.labelFontSize,
-                config.labelOffsetY, config.labelMargin);
+                config.slotViewSpec, config.labelSpec);
         mAlbumSetView.setListener(new SlotView.SimpleListener() {
             @Override
+            public void onDown(int index) {
+                ManageCachePage.this.onDown(index);
+            }
+
+            @Override
+            public void onUp() {
+                ManageCachePage.this.onUp();
+            }
+
+            @Override
             public void onSingleTapUp(int slotIndex) {
                 ManageCachePage.this.onSingleTapUp(slotIndex);
             }
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
index 4c9be7e..6bc6fdc 100644
--- a/src/com/android/gallery3d/app/MovieActivity.java
+++ b/src/com/android/gallery3d/app/MovieActivity.java
@@ -27,6 +27,7 @@
 import android.os.Bundle;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Video.VideoColumns;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
@@ -51,8 +52,8 @@
         setContentView(R.layout.movie_view);
         View rootView = findViewById(R.id.root);
         Intent intent = getIntent();
-        setVideoTitle(intent);
-        mPlayer = new MoviePlayer(rootView, this, intent.getData()) {
+        initializeActionBar(intent);
+        mPlayer = new MoviePlayer(rootView, this, intent.getData(), savedInstanceState) {
             @Override
             public void onCompletion() {
                 if (mFinishOnCompletion) {
@@ -73,10 +74,12 @@
         WindowManager.LayoutParams winParams = win.getAttributes();
         winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
         win.setAttributes(winParams);
-
     }
 
-    private void setVideoTitle(Intent intent) {
+    private void initializeActionBar(Intent intent) {
+        ActionBar actionBar = getActionBar();
+        actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
+                ActionBar.DISPLAY_HOME_AS_UP);
         String title = intent.getStringExtra(Intent.EXTRA_TITLE);
         if (title == null) {
             Cursor cursor = null;
@@ -92,8 +95,16 @@
                 if (cursor != null) cursor.close();
             }
         }
-        ActionBar actionBar = getActionBar();
-        if (title != null && actionBar != null) actionBar.setTitle(title);
+        if (title != null) actionBar.setTitle(title);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+            return true;
+        }
+        return false;
     }
 
     @Override
@@ -124,6 +135,12 @@
     }
 
     @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        mPlayer.onSaveInstanceState(outState);
+    }
+
+    @Override
     public void onDestroy() {
         mPlayer.onDestroy();
         super.onDestroy();
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
index 4239944..ee76fa5 100644
--- a/src/com/android/gallery3d/app/MoviePlayer.java
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -33,6 +33,7 @@
 import android.media.AudioManager;
 import android.media.MediaPlayer;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.Handler;
 import android.view.KeyEvent;
 import android.view.View;
@@ -49,11 +50,18 @@
     @SuppressWarnings("unused")
     private static final String TAG = "MoviePlayer";
 
+    private static final String KEY_VIDEO_POSITION = "video-position";
+    private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
+
     // Copied from MediaPlaybackService in the Music Player app.
     private static final String SERVICECMD = "com.android.music.musicservicecommand";
     private static final String CMDNAME = "command";
     private static final String CMDPAUSE = "pause";
 
+    // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing.
+    // Otherwise, we pause the player.
+    private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins
+
     private Context mContext;
     private final VideoView mVideoView;
     private final View mProgressView;
@@ -62,10 +70,14 @@
     private final Handler mHandler = new Handler();
     private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
     private final ActionBar mActionBar;
+    private final MediaController mMediaController;
 
-    private boolean mHasPaused;
+    private long mResumeableTime = Long.MAX_VALUE;
+    private int mVideoPosition = 0;
+    private boolean mHasPaused = false;
 
     private final Runnable mPlayingChecker = new Runnable() {
+        @Override
         public void run() {
             if (mVideoView.isPlaying()) {
                 mProgressView.setVisibility(View.GONE);
@@ -75,7 +87,8 @@
         }
     };
 
-    public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri) {
+    public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri,
+            Bundle savedInstance) {
         mContext = movieActivity.getApplicationContext();
         mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
         mProgressView = rootView.findViewById(R.id.progress_indicator);
@@ -96,7 +109,7 @@
         mVideoView.setOnCompletionListener(this);
         mVideoView.setVideoURI(mUri);
 
-        MediaController mediaController = new MediaController(movieActivity) {
+        mMediaController = new MediaController(movieActivity) {
             @Override
             public void show() {
                 super.show();
@@ -109,8 +122,8 @@
                 mActionBar.hide();
             }
         };
-        mVideoView.setMediaController(mediaController);
-        mediaController.setOnKeyListener(new View.OnKeyListener() {
+        mMediaController.setOnKeyListener(new View.OnKeyListener() {
+            @Override
             public boolean onKey(View v, int keyCode, KeyEvent event) {
                 if (keyCode == KeyEvent.KEYCODE_BACK) {
                     if (event.getAction() == KeyEvent.ACTION_UP) {
@@ -121,6 +134,7 @@
                 return false;
             }
         });
+        mVideoView.setMediaController(mMediaController);
 
         mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
         mAudioBecomingNoisyReceiver.register();
@@ -132,14 +146,27 @@
         i.putExtra(CMDNAME, CMDPAUSE);
         movieActivity.sendBroadcast(i);
 
-        final Integer bookmark = mBookmarker.getBookmark(mUri);
-        if (bookmark != null) {
-            showResumeDialog(movieActivity, bookmark);
-        } else {
+        if (savedInstance != null) { // this is a resumed activity
+            mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
+            mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
             mVideoView.start();
+            mVideoView.suspend();
+            mHasPaused = true;
+        } else {
+            final Integer bookmark = mBookmarker.getBookmark(mUri);
+            if (bookmark != null) {
+                showResumeDialog(movieActivity, bookmark);
+            } else {
+                mVideoView.start();
+            }
         }
     }
 
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
+        outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
+    }
+
     private void showResumeDialog(Context context, final int bookmark) {
         AlertDialog.Builder builder = new AlertDialog.Builder(context);
         builder.setTitle(R.string.resume_playing_title);
@@ -147,12 +174,14 @@
                 context.getString(R.string.resume_playing_message),
                 GalleryUtils.formatDuration(context, bookmark / 1000)));
         builder.setOnCancelListener(new OnCancelListener() {
+            @Override
             public void onCancel(DialogInterface dialog) {
                 onCompletion();
             }
         });
         builder.setPositiveButton(
                 R.string.resume_playing_resume, new OnClickListener() {
+            @Override
             public void onClick(DialogInterface dialog, int which) {
                 mVideoView.seekTo(bookmark);
                 mVideoView.start();
@@ -160,6 +189,7 @@
         });
         builder.setNegativeButton(
                 R.string.resume_playing_restart, new OnClickListener() {
+            @Override
             public void onClick(DialogInterface dialog, int which) {
                 mVideoView.start();
             }
@@ -168,21 +198,25 @@
     }
 
     public void onPause() {
-        mHandler.removeCallbacksAndMessages(null);
-        mBookmarker.setBookmark(mUri, mVideoView.getCurrentPosition(),
-                mVideoView.getDuration());
-        mVideoView.suspend();
         mHasPaused = true;
+        mHandler.removeCallbacksAndMessages(null);
+        mVideoPosition = mVideoView.getCurrentPosition();
+        mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
+        mVideoView.suspend();
+        mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
     }
 
     public void onResume() {
         if (mHasPaused) {
-            Integer bookmark = mBookmarker.getBookmark(mUri);
-            if (bookmark != null) {
-                mVideoView.seekTo(bookmark);
+            mVideoView.seekTo(mVideoPosition);
+            mVideoView.resume();
+
+            // If we have slept for too long, pause the play
+            if (System.currentTimeMillis() > mResumeableTime) {
+                mMediaController.show();
+                mVideoView.pause();
             }
         }
-        mVideoView.resume();
     }
 
     public void onDestroy() {
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
index a3b385a..44b9299 100644
--- a/src/com/android/gallery3d/app/PhotoPage.java
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -85,6 +85,7 @@
     private FilmStripView mFilmStripView;
     private DetailsHelper mDetailsHelper;
     private boolean mShowDetails;
+    private Path mPendingSharePath;
 
     // mMediaSet could be null if there is no KEY_MEDIA_SET_PATH supplied.
     // E.g., viewing a photo in gmail attachment
@@ -240,6 +241,7 @@
             mPhotoView.setModel(mModel);
             updateCurrentPhoto(mediaItem);
         }
+
         mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
             @Override
             public void handleMessage(Message message) {
@@ -257,6 +259,21 @@
         mPhotoView.setOpenedItem(itemPath);
     }
 
+    private void updateShareURI(Path path) {
+        if (mShareActionProvider != null) {
+            DataManager manager = mActivity.getDataManager();
+            int type = manager.getMediaType(path);
+            Intent intent = new Intent(Intent.ACTION_SEND);
+            intent.setType(MenuExecutor.getMimeType(type));
+            intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
+            mShareActionProvider.setShareIntent(intent);
+            mPendingSharePath = null;
+        } else {
+            // This happens when ActionBar is not created yet.
+            mPendingSharePath = path;
+        }
+    }
+
     private void setTitle(String title) {
         if (title == null) return;
         boolean showTitle = mActivity.getAndroidContext().getResources().getBoolean(
@@ -276,19 +293,10 @@
             mDetailsHelper.reloadDetails(mModel.getCurrentIndex());
         }
         setTitle(photo.getName());
-        mPhotoView.showVideoPlayIcon(photo.getMediaType()
-                == MediaObject.MEDIA_TYPE_VIDEO);
+        mPhotoView.showVideoPlayIcon(
+                photo.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO);
 
-        // If we have an ActionBar then we update the share intent
-        if (mShareActionProvider != null) {
-            Path path = photo.getPath();
-            DataManager manager = mActivity.getDataManager();
-            int type = manager.getMediaType(path);
-            Intent intent = new Intent(Intent.ACTION_SEND);
-            intent.setType(MenuExecutor.getMimeType(type));
-            intent.putExtra(Intent.EXTRA_STREAM, manager.getContentUri(path));
-            mShareActionProvider.setShareIntent(intent);
-        }
+        updateShareURI(photo.getPath());
     }
 
     private void updateMenuOperations() {
@@ -384,6 +392,7 @@
         menu.findItem(R.id.action_slideshow).setVisible(
                 mMediaSet != null && !(mMediaSet instanceof MtpDevice));
         mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
+        if (mPendingSharePath != null) updateShareURI(mPendingSharePath);
         mMenu = menu;
         mShowBars = true;
         updateMenuOperations();
diff --git a/src/com/android/gallery3d/app/PickerActivity.java b/src/com/android/gallery3d/app/PickerActivity.java
new file mode 100644
index 0000000..944192d
--- /dev/null
+++ b/src/com/android/gallery3d/app/PickerActivity.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.Window;
+
+public class PickerActivity extends AbstractGalleryActivity
+        implements OnClickListener {
+
+    public static final String KEY_ALBUM_PATH = "album-path";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // We show the picker in two ways. One smaller screen we use a full
+        // screen window with an action bar. On larger screen we use a dialog.
+        boolean isDialog = getResources().getBoolean(R.bool.picker_is_dialog);
+
+        if (!isDialog) {
+            requestWindowFeature(Window.FEATURE_ACTION_BAR);
+            requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+        }
+
+        setContentView(R.layout.dialog_picker);
+
+        if (isDialog) {
+            // In dialog mode, we don't have the action bar to show the
+            // "cancel" action, so we show an additional "cancel" button.
+            View view = findViewById(R.id.cancel);
+            view.setOnClickListener(this);
+            view.setVisibility(View.VISIBLE);
+
+            // We need this, otherwise the view will be dimmed because it
+            // is "behind" the dialog.
+            ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.pickup, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.action_cancel) {
+            finish();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onBackPressed() {
+        // send the back event to the top sub-state
+        GLRoot root = getGLRoot();
+        root.lockRenderThread();
+        try {
+            getStateManager().getTopState().onBackPressed();
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v.getId() == R.id.cancel) finish();
+    }
+}
diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java
index afd5faa..29b2aa7 100644
--- a/src/com/android/gallery3d/data/DecodeUtils.java
+++ b/src/com/android/gallery3d/data/DecodeUtils.java
@@ -102,8 +102,12 @@
         options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
                 options.outWidth, options.outHeight, targetSize);
         options.inJustDecodeBounds = false;
-        return ensureGLCompatibleBitmap(
-                BitmapFactory.decodeFileDescriptor(fd, null, options));
+
+        Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options);
+        // We need to resize down if the decoder does not support inSampleSize.
+        // (For example, GIF images.)
+        result = BitmapUtils.resizeDownIfTooBig(result, targetSize, true);
+        return ensureGLCompatibleBitmap(result);
     }
 
     /**
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index 7ab04c5..e70b2ee 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -61,6 +61,8 @@
     private static final int INDEX_ORIENTATION = 9;
     private static final int INDEX_BUCKET_ID = 10;
     private static final int INDEX_SIZE_ID = 11;
+    private static final int INDEX_WIDTH = 12;
+    private static final int INDEX_HEIGHT = 13;
 
     static final String[] PROJECTION =  {
             ImageColumns._ID,           // 0
@@ -74,12 +76,17 @@
             ImageColumns.DATA,          // 8
             ImageColumns.ORIENTATION,   // 9
             ImageColumns.BUCKET_ID,     // 10
-            ImageColumns.SIZE           // 11
+            ImageColumns.SIZE,          // 11
+            // These should be changed to proper names after they are made public.
+            "width", // ImageColumns.WIDTH,         // 12
+            "height", // ImageColumns.HEIGHT         // 13
     };
 
     private final GalleryApp mApplication;
 
     public int rotation;
+    public int width;
+    public int height;
 
     public LocalImage(Path path, GalleryApp application, Cursor cursor) {
         super(path, nextVersionNumber());
@@ -118,6 +125,8 @@
         rotation = cursor.getInt(INDEX_ORIENTATION);
         bucketId = cursor.getInt(INDEX_BUCKET_ID);
         fileSize = cursor.getLong(INDEX_SIZE_ID);
+        width = cursor.getInt(INDEX_WIDTH);
+        height = cursor.getInt(INDEX_HEIGHT);
     }
 
     @Override
@@ -138,6 +147,8 @@
         rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION));
         bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
         fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE_ID));
+        width = uh.update(width, cursor.getInt(INDEX_WIDTH));
+        height = uh.update(height, cursor.getInt(INDEX_HEIGHT));
         return uh.isUpdated();
     }
 
@@ -306,4 +317,14 @@
     public int getRotation() {
         return rotation;
     }
+
+    @Override
+    public int getWidth() {
+        return width;
+    }
+
+    @Override
+    public int getHeight() {
+        return height;
+    }
 }
diff --git a/src/com/android/gallery3d/data/LocalVideo.java b/src/com/android/gallery3d/data/LocalVideo.java
index d1498e8..111a5f1 100644
--- a/src/com/android/gallery3d/data/LocalVideo.java
+++ b/src/com/android/gallery3d/data/LocalVideo.java
@@ -210,4 +210,14 @@
         }
         return details;
     }
+
+    @Override
+    public int getWidth() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return 0;
+    }
 }
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
index 0906251..a0c6d8c 100644
--- a/src/com/android/gallery3d/data/MediaItem.java
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -78,4 +78,9 @@
     }
 
     public abstract String getMimeType();
+
+    // Returns width and height of the media item.
+    // Returns 0, 0 if the information is not available.
+    public abstract int getWidth();
+    public abstract int getHeight();
 }
diff --git a/src/com/android/gallery3d/data/MtpImage.java b/src/com/android/gallery3d/data/MtpImage.java
index 218f704..211b2f2 100644
--- a/src/com/android/gallery3d/data/MtpImage.java
+++ b/src/com/android/gallery3d/data/MtpImage.java
@@ -157,4 +157,13 @@
         return details;
     }
 
+    @Override
+    public int getWidth() {
+        return mImageWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mImageHeight;
+    }
 }
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
index 3a7ed7c..e97b035 100644
--- a/src/com/android/gallery3d/data/UriImage.java
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -263,4 +263,14 @@
             super.finalize();
         }
     }
+
+    @Override
+    public int getWidth() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return 0;
+    }
 }
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
index 91a98ce..b8d049b 100644
--- a/src/com/android/gallery3d/ui/ActionModeHandler.java
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -86,7 +86,7 @@
         mSelectionMenu = customMenu.addDropDownMenu(
                 (Button) customView.findViewById(R.id.selection_menu),
                 R.menu.selection);
-        updateSelectAllTitle();
+        updateSelectionMenu();
         customMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
             public boolean onMenuItemClick(MenuItem item) {
                 return onActionItemClicked(actionMode, item);
@@ -119,12 +119,17 @@
         result = mMenuExecutor.onMenuClicked(item, listener);
         if (item.getItemId() == R.id.action_select_all) {
             updateSupportedOperation();
-            updateSelectAllTitle();
+            updateSelectionMenu();
         }
         return result;
     }
 
-    private void updateSelectAllTitle() {
+    private void updateSelectionMenu() {
+        // update title
+        int count = mSelectionManager.getSelectedCount();
+        String format = mActivity.getResources().getQuantityString(
+                R.plurals.number_of_items_selected, count);
+        setTitle(String.format(format, count));
         // For clients who call SelectionManager.selectAll() directly, we need to ensure the
         // menu status is consistent with selection manager.
         MenuItem item = mSelectionMenu.findItem(R.id.action_select_all);
@@ -164,26 +169,25 @@
         return true;
     }
 
-    private void updateMenuOptionsAndSharingIntent(JobContext jc) {
-        ArrayList<Path> paths = mSelectionManager.getSelected(true);
+    // Menu options are determined by selection set itself.
+    // We cannot expand it because MenuExecuter executes it based on
+    // the selection set instead of the expanded result.
+    // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't.
+    private void updateMenuOptions(JobContext jc) {
+        ArrayList<Path> paths = mSelectionManager.getSelected(false);
         if (paths.size() == 0) return;
 
         int operation = MediaObject.SUPPORT_ALL;
         DataManager manager = mActivity.getDataManager();
-        final ArrayList<Uri> uris = new ArrayList<Uri>();
         int type = 0;
         for (Path path : paths) {
             if (jc.isCancelled()) return;
             int support = manager.getSupportedOperations(path);
             type |= manager.getMediaType(path);
             operation &= support;
-            if ((support & MediaObject.SUPPORT_SHARE) != 0) {
-                uris.add(manager.getContentUri(path));
-            }
         }
-        final Intent intent = new Intent();
-        final String mimeType = MenuExecutor.getMimeType(type);
 
+        final String mimeType = MenuExecutor.getMimeType(type);
         if (paths.size() == 1) {
             if (!GalleryUtils.isEditorAvailable((Context) mActivity, mimeType)) {
                 operation &= ~MediaObject.SUPPORT_EDIT;
@@ -192,19 +196,6 @@
             operation &= SUPPORT_MULTIPLE_MASK;
         }
 
-        final int size = uris.size();
-        Log.v(TAG, "Sharing intent MIME type=" + mimeType + ", uri size = "+ uris.size());
-        if (size > 0) {
-            if (size > 1) {
-                intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType);
-                intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
-            } else {
-                intent.setAction(Intent.ACTION_SEND).setType(mimeType);
-                intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
-            }
-            intent.setType(mimeType);
-        }
-
         final int supportedOperation = operation;
 
         mMainHandler.post(new Runnable() {
@@ -212,13 +203,52 @@
             public void run() {
                 mMenuTask = null;
                 MenuExecutor.updateMenuOperation(mMenu, supportedOperation);
+            }
+        });
+    }
 
-                if (mShareActionProvider != null && size > 0) {
+    // Share intent needs to expand the selection set so we can get URI of
+    // each media item
+    private void updateSharingIntent(JobContext jc) {
+        if (mShareActionProvider == null) return;
+        ArrayList<Path> paths = mSelectionManager.getSelected(true);
+        if (paths.size() == 0) return;
+
+        final ArrayList<Uri> uris = new ArrayList<Uri>();
+
+        DataManager manager = mActivity.getDataManager();
+        int type = 0;
+
+        final Intent intent = new Intent();
+        for (Path path : paths) {
+            int support = manager.getSupportedOperations(path);
+            type |= manager.getMediaType(path);
+
+            if ((support & MediaObject.SUPPORT_SHARE) != 0) {
+                uris.add(manager.getContentUri(path));
+            }
+        }
+
+        final int size = uris.size();
+        if (size > 0) {
+            final String mimeType = MenuExecutor.getMimeType(type);
+            if (size > 1) {
+                intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType);
+                intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+            } else {
+                intent.setAction(Intent.ACTION_SEND).setType(mimeType);
+                intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+            }
+            intent.setType(mimeType);
+
+            mMainHandler.post(new Runnable() {
+                @Override
+                public void run() {
                     Log.v(TAG, "Sharing intent is ready: action = " + intent.getAction());
                     mShareActionProvider.setShareIntent(intent);
                 }
-            }
-        });
+            });
+        }
     }
 
     public void updateSupportedOperation(Path path, boolean selected) {
@@ -240,7 +270,8 @@
         // Generate sharing intent and update supported operations in the background
         mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() {
             public Void run(JobContext jc) {
-                updateMenuOptionsAndSharingIntent(jc);
+                updateMenuOptions(jc);
+                updateSharingIntent(jc);
                 return null;
             }
         });
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
index 92d8b41..3b36397 100644
--- a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
+++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
@@ -25,6 +25,7 @@
 import com.android.gallery3d.ui.AlbumSetView.AlbumSetItem;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.MediaSetUtils;
 import com.android.gallery3d.util.ThreadPool;
 
@@ -45,9 +46,7 @@
 
     private final AlbumSetView.Model mSource;
     private int mSize;
-    private int mLabelWidth;
-    private int mDisplayItemSize;
-    private int mLabelFontSize;
+    private AlbumSetView.LabelSpec mLabelSpec;
 
     private int mContentStart = 0;
     private int mContentEnd = 0;
@@ -75,13 +74,11 @@
         public int cacheStatus;
     }
 
-    public AlbumSetSlidingWindow(GalleryActivity activity, int labelWidth,
-            int displayItemSize, int labelFontSize, SelectionDrawer drawer,
+    public AlbumSetSlidingWindow(GalleryActivity activity,
+            AlbumSetView.LabelSpec labelSpec, SelectionDrawer drawer,
             AlbumSetView.Model source, int cacheSize) {
         source.setModelListener(this);
-        mLabelWidth = labelWidth;
-        mDisplayItemSize = displayItemSize;
-        mLabelFontSize = labelFontSize;
+        mLabelSpec = labelSpec;
         mLoadingLabel = activity.getAndroidContext().getString(R.string.loading);
         mSource = source;
         mSelectionDrawer = drawer;
@@ -341,6 +338,7 @@
         private final int mMediaType;
         private Texture mContent;
         private final long mDataVersion;
+        private boolean mIsPanorama;
 
         public GalleryDisplayItem(int slotIndex, int coverIndex, MediaItem item) {
             super(item);
@@ -348,6 +346,7 @@
             mCoverIndex = coverIndex;
             mMediaType = item.getMediaType();
             mDataVersion = item.getDataVersion();
+            mIsPanorama = GalleryUtils.isPanorama(item);
             updateContent(mWaitLoadingTexture);
         }
 
@@ -358,7 +357,7 @@
                 if (mActiveRequestCount == 0) requestNonactiveImages();
             }
             if (bitmap != null) {
-                BitmapTexture texture = new BitmapTexture(bitmap);
+                BitmapTexture texture = new BitmapTexture(bitmap, true);
                 texture.setThrottled(true);
                 updateContent(texture);
                 if (mListener != null) mListener.onContentInvalidated();
@@ -367,20 +366,22 @@
 
         private void updateContent(Texture content) {
             mContent = content;
-
-            int width = content.getWidth();
-            int height = content.getHeight();
-
-            float scale = (float) mDisplayItemSize / Math.max(width, height);
-
-            width = (int) Math.floor(width * scale);
-            height = (int) Math.floor(height * scale);
-
-            setSize(width, height);
         }
 
         @Override
         public boolean render(GLCanvas canvas, int pass) {
+            // Fit the content into the box
+            int width = mContent.getWidth();
+            int height = mContent.getHeight();
+
+            float scalex = mBoxWidth / (float) width;
+            float scaley = mBoxHeight / (float) height;
+            float scale = Math.min(scalex, scaley);
+
+            width = (int) Math.floor(width * scale);
+            height = (int) Math.floor(height * scale);
+
+            // Now draw it
             int sourceType = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
             int cacheFlag = MediaSet.CACHE_FLAG_NO;
             int cacheStatus = MediaSet.CACHE_STATUS_NOT_CACHED;
@@ -392,8 +393,9 @@
                 cacheStatus = set.cacheStatus;
             }
 
-            mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight,
+            mSelectionDrawer.draw(canvas, mContent, width, height,
                     getRotation(), path, mCoverIndex, sourceType, mMediaType,
+                    mIsPanorama, mLabelSpec.labelBackgroundHeight,
                     cacheFlag == MediaSet.CACHE_FLAG_FULL,
                     (cacheFlag == MediaSet.CACHE_FLAG_FULL)
                     && (cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL));
@@ -471,37 +473,65 @@
     }
 
     private class LabelDisplayItem extends DisplayItem {
-        private static final int FONT_COLOR = Color.WHITE;
+        private static final int FONT_COLOR_TITLE = Color.WHITE;
+        private static final int FONT_COLOR_COUNT = 0x80FFFFFF;  // 50% white
 
-        private StringTexture mTexture;
-        private String mLabel;
-        private String mPostfix;
+        private StringTexture mTextureTitle;
+        private StringTexture mTextureCount;
+        private String mTitle;
+        private String mCount;
+        private int mLastWidth;
         private final int mSlotIndex;
+        private boolean mHasIcon;
 
         public LabelDisplayItem(int slotIndex) {
             mSlotIndex = slotIndex;
-            updateContent();
         }
 
         public boolean updateContent() {
-            String label = mLoadingLabel;
-            String postfix = null;
+            String title = mLoadingLabel;
+            String count = "";
             MediaSet set = mSource.getMediaSet(mSlotIndex);
             if (set != null) {
-                label = Utils.ensureNotNull(set.getName());
-                postfix = " (" + set.getTotalMediaItemCount() + ")";
+                title = Utils.ensureNotNull(set.getName());
+                count = "" + set.getTotalMediaItemCount();
             }
-            if (Utils.equals(label, mLabel)
-                    && Utils.equals(postfix, mPostfix)) return false;
-            mTexture = StringTexture.newInstance(
-                    label, postfix, mLabelFontSize, FONT_COLOR, mLabelWidth, true);
-            setSize(mTexture.getWidth(), mTexture.getHeight());
+            if (Utils.equals(title, mTitle)
+                    && Utils.equals(count, mCount)
+                    && Utils.equals(mBoxWidth, mLastWidth)) {
+                    return false;
+            }
+            mTitle = title;
+            mCount = count;
+            mLastWidth = mBoxWidth;
+            mHasIcon = (identifySourceType(set) !=
+                    SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED);
+
+            AlbumSetView.LabelSpec s = mLabelSpec;
+            mTextureTitle = StringTexture.newInstance(
+                    title, s.titleFontSize, FONT_COLOR_TITLE,
+                    mBoxWidth - s.leftMargin, false);
+            mTextureCount = StringTexture.newInstance(
+                    count, s.countFontSize, FONT_COLOR_COUNT,
+                    mBoxWidth - s.leftMargin, true);
+
             return true;
         }
 
         @Override
         public boolean render(GLCanvas canvas, int pass) {
-            mTexture.draw(canvas, -mWidth / 2, -mHeight / 2);
+            if (mBoxWidth != mLastWidth) {
+                updateContent();
+            }
+
+            AlbumSetView.LabelSpec s = mLabelSpec;
+            int x = -mBoxWidth / 2;
+            int y = (mBoxHeight + 1) / 2 - s.labelBackgroundHeight;
+            y += s.titleOffset;
+            mTextureTitle.draw(canvas, x + s.leftMargin, y);
+            y += s.titleFontSize + s.countOffset;
+            x += mHasIcon ? s.iconSize : s.leftMargin;
+            mTextureCount.draw(canvas, x, y);
             return false;
         }
 
diff --git a/src/com/android/gallery3d/ui/AlbumSetView.java b/src/com/android/gallery3d/ui/AlbumSetView.java
index ef066b3..89dfe4a 100644
--- a/src/com/android/gallery3d/ui/AlbumSetView.java
+++ b/src/com/android/gallery3d/ui/AlbumSetView.java
@@ -40,11 +40,7 @@
 
     private AlbumSetSlidingWindow mDataWindow;
     private final GalleryActivity mActivity;
-    private final int mSlotWidth;
-    private final int mDisplayItemSize;
-    private final int mLabelFontSize;
-    private final int mLabelOffsetY;
-    private final int mLabelMargin;
+    private final LabelSpec mLabelSpec;
 
     private SelectionDrawer mSelectionDrawer;
 
@@ -67,18 +63,23 @@
         public long setDataVersion;
     }
 
+    public static class LabelSpec {
+        public int labelBackgroundHeight;
+        public int titleOffset;
+        public int countOffset;
+        public int titleFontSize;
+        public int countFontSize;
+        public int leftMargin;
+        public int iconSize;
+    }
+
     public AlbumSetView(GalleryActivity activity, SelectionDrawer drawer,
-            int slotWidth, int slotHeight, int displayItemSize,
-            int labelFontSize, int labelOffsetY, int labelMargin) {
+            SlotView.Spec slotViewSpec, LabelSpec labelSpec) {
         super(activity.getAndroidContext());
         mActivity = activity;
         setSelectionDrawer(drawer);
-        setSlotSize(slotWidth, slotHeight);
-        mSlotWidth = slotWidth;
-        mDisplayItemSize = displayItemSize;
-        mLabelFontSize = labelFontSize;
-        mLabelOffsetY = labelOffsetY;
-        mLabelMargin = labelMargin;
+        setSlotSpec(slotViewSpec);
+        mLabelSpec = labelSpec;
     }
 
     public void setSelectionDrawer(SelectionDrawer drawer) {
@@ -95,8 +96,7 @@
             mDataWindow = null;
         }
         if (model != null) {
-            mDataWindow = new AlbumSetSlidingWindow(mActivity,
-                    mSlotWidth - mLabelMargin * 2, mDisplayItemSize, mLabelFontSize,
+            mDataWindow = new AlbumSetSlidingWindow(mActivity, mLabelSpec,
                     mSelectionDrawer, model, CACHE_SIZE);
             mDataWindow.setListener(new MyCacheListener());
             setSlotCount(mDataWindow.size());
@@ -119,8 +119,7 @@
 
         // Put the cover items in reverse order, so that the first item is on
         // top of the rest.
-        int labelY = y + mLabelOffsetY - entry.labelItem.getHeight() / 2;
-        Position position = new Position(x, labelY, 0f);
+        Position position = new Position(x, y, 0f);
         putDisplayItem(position, position, entry.labelItem);
 
         for (int i = 0, n = items.length; i < n; ++i) {
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
index 9b410e9..5f71434 100644
--- a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -24,6 +24,7 @@
 import com.android.gallery3d.data.Path;
 import com.android.gallery3d.util.Future;
 import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.JobLimiter;
 import com.android.gallery3d.util.ThreadPool.Job;
 import com.android.gallery3d.util.ThreadPool.JobContext;
@@ -38,7 +39,6 @@
 
     private static final int MSG_LOAD_BITMAP_DONE = 0;
     private static final int MSG_UPDATE_SLOT = 1;
-    private static final int MIN_THUMB_SIZE = 100;
     private static final int JOB_LIMIT = 2;
 
     public static interface Listener {
@@ -66,24 +66,20 @@
 
     private SynchronizedHandler mHandler;
     private JobLimiter mThreadPool;
-    private int mSlotWidth, mSlotHeight;
 
     private int mActiveRequestCount = 0;
     private boolean mIsActive = false;
 
-    private int mDisplayItemSize;  // 0: disabled
+    private int mCacheThumbSize;  // 0: Don't cache the thumbnails
     private LruCache<Path, Bitmap> mImageCache = new LruCache<Path, Bitmap>(1000);
 
     public AlbumSlidingWindow(GalleryActivity activity,
             AlbumView.Model source, int cacheSize,
-            int slotWidth, int slotHeight, int displayItemSize) {
+            int cacheThumbSize) {
         source.setModelListener(this);
         mSource = source;
         mData = new AlbumDisplayItem[cacheSize];
         mSize = source.size();
-        mSlotWidth = slotWidth;
-        mSlotHeight = slotHeight;
-        mDisplayItemSize = displayItemSize;
 
         mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT);
         mWaitLoadingTexture.setSize(1, 1);
@@ -288,6 +284,7 @@
         private final int mSlotIndex;
         private final int mMediaType;
         private Texture mContent;
+        private boolean mIsPanorama;
 
         public AlbumDisplayItem(int slotIndex, MediaItem item) {
             super(item);
@@ -295,6 +292,7 @@
                     ? MediaItem.MEDIA_TYPE_UNKNOWN
                     : item.getMediaType();
             mSlotIndex = slotIndex;
+            mIsPanorama = GalleryUtils.isPanorama(item);
             updateContent(mWaitLoadingTexture);
         }
 
@@ -306,7 +304,7 @@
                 if (mActiveRequestCount == 0) requestNonactiveImages();
             }
             if (bitmap != null) {
-                BitmapTexture texture = new BitmapTexture(bitmap);
+                BitmapTexture texture = new BitmapTexture(bitmap, true);
                 texture.setThrottled(true);
                 updateContent(texture);
                 if (mListener != null && isActiveSlot) {
@@ -317,37 +315,37 @@
 
         private void updateContent(Texture content) {
             mContent = content;
+        }
 
+        @Override
+        public boolean render(GLCanvas canvas, int pass) {
+            // Fit the content into the box
             int width = mContent.getWidth();
             int height = mContent.getHeight();
 
-            float scalex = mDisplayItemSize / (float) width;
-            float scaley = mDisplayItemSize / (float) height;
+            float scalex = mBoxWidth / (float) width;
+            float scaley = mBoxHeight / (float) height;
             float scale = Math.min(scalex, scaley);
 
             width = (int) Math.floor(width * scale);
             height = (int) Math.floor(height * scale);
 
-            setSize(width, height);
-        }
-
-        @Override
-        public boolean render(GLCanvas canvas, int pass) {
+            // Now draw it
             if (pass == 0) {
                 Path path = null;
                 if (mMediaItem != null) path = mMediaItem.getPath();
-                mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight,
-                        getRotation(), path, mMediaType);
+                mSelectionDrawer.draw(canvas, mContent, width, height,
+                        getRotation(), path, mMediaType, mIsPanorama);
                 return (mFocusIndex == mSlotIndex);
             } else if (pass == 1) {
-                mSelectionDrawer.drawFocus(canvas, mWidth, mHeight);
+                mSelectionDrawer.drawFocus(canvas, width, height);
             }
             return false;
         }
 
         @Override
         public void startLoadBitmap() {
-            if (mDisplayItemSize < MIN_THUMB_SIZE) {
+            if (mCacheThumbSize > 0) {
                 Path path = mMediaItem.getPath();
                 if (mImageCache.containsKey(path)) {
                     Bitmap bitmap = mImageCache.get(path);
@@ -368,7 +366,7 @@
             Bitmap bitmap = job.run(jc);
             if (bitmap != null) {
                 bitmap = BitmapUtils.resizeDownBySideLength(
-                        bitmap, mDisplayItemSize, true);
+                        bitmap, mCacheThumbSize, true);
             }
             return bitmap;
         }
@@ -390,7 +388,7 @@
             mFuture = null;
             Bitmap bitmap = future.get();
             boolean isCancelled = future.isCancelled();
-            if (mDisplayItemSize < MIN_THUMB_SIZE && (bitmap != null || !isCancelled)) {
+            if (mCacheThumbSize > 0 && (bitmap != null || !isCancelled)) {
                 Path path = mMediaItem.getPath();
                 mImageCache.put(path, bitmap);
             }
diff --git a/src/com/android/gallery3d/ui/AlbumView.java b/src/com/android/gallery3d/ui/AlbumView.java
index 417611a..128259a 100644
--- a/src/com/android/gallery3d/ui/AlbumView.java
+++ b/src/com/android/gallery3d/ui/AlbumView.java
@@ -33,8 +33,7 @@
     private AlbumSlidingWindow mDataWindow;
     private final GalleryActivity mActivity;
     private SelectionDrawer mSelectionDrawer;
-    private int mSlotWidth, mSlotHeight;
-    private int mDisplayItemSize;
+    private int mCacheThumbSize;
 
     private boolean mIsActive = false;
 
@@ -50,13 +49,11 @@
         public void onSizeChanged(int size);
     }
 
-    public AlbumView(GalleryActivity activity,
-            int slotWidth, int slotHeight, int displayItemSize) {
+    public AlbumView(GalleryActivity activity, SlotView.Spec spec,
+            int cacheThumbSize) {
         super(activity.getAndroidContext());
-        mSlotWidth = slotWidth;
-        mSlotHeight = slotHeight;
-        mDisplayItemSize = displayItemSize;
-        setSlotSize(slotWidth, slotHeight);
+        mCacheThumbSize = cacheThumbSize;
+        setSlotSpec(spec);
         mActivity = activity;
     }
 
@@ -74,7 +71,7 @@
         if (model != null) {
             mDataWindow = new AlbumSlidingWindow(
                     mActivity, model, CACHE_SIZE,
-                    mSlotWidth, mSlotHeight, mDisplayItemSize);
+                    mCacheThumbSize);
             mDataWindow.setSelectionDrawer(mSelectionDrawer);
             mDataWindow.setListener(new MyDataModelListener());
             setSlotCount(model.size());
diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java
index e930063..8946036 100644
--- a/src/com/android/gallery3d/ui/BasicTexture.java
+++ b/src/com/android/gallery3d/ui/BasicTexture.java
@@ -43,6 +43,8 @@
     private int mTextureWidth;
     private int mTextureHeight;
 
+    private boolean mHasBorder;
+
     protected WeakReference<GLCanvas> mCanvasRef = null;
     private static WeakHashMap<BasicTexture, Object> sAllTextures
             = new WeakHashMap<BasicTexture, Object>();
@@ -100,6 +102,25 @@
         return mTextureHeight;
     }
 
+    // Returns true if the texture has one pixel transparent border around the
+    // actual content. This is used to avoid jigged edges.
+    //
+    // The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap
+    // mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially
+    // covered by the texture will use the color of the edge texel. If we add
+    // the transparent border, the color of the edge texel will be mixed with
+    // appropriate amount of transparent.
+    //
+    // Currently our background is black, so we can draw the thumbnails without
+    // enabling blending.
+    public boolean hasBorder() {
+        return mHasBorder;
+    }
+
+    protected void setBorder(boolean hasBorder) {
+        mHasBorder = hasBorder;
+    }
+
     public void draw(GLCanvas canvas, int x, int y) {
         canvas.drawTexture(this, x, y, getWidth(), getHeight());
     }
diff --git a/src/com/android/gallery3d/ui/BitmapTexture.java b/src/com/android/gallery3d/ui/BitmapTexture.java
index 046bda9..f5cb2bd 100644
--- a/src/com/android/gallery3d/ui/BitmapTexture.java
+++ b/src/com/android/gallery3d/ui/BitmapTexture.java
@@ -29,6 +29,11 @@
     protected Bitmap mContentBitmap;
 
     public BitmapTexture(Bitmap bitmap) {
+        this(bitmap, false);
+    }
+
+    public BitmapTexture(Bitmap bitmap, boolean hasBorder) {
+        super(hasBorder);
         Utils.assertTrue(bitmap != null && !bitmap.isRecycled());
         mContentBitmap = bitmap;
     }
diff --git a/src/com/android/gallery3d/ui/DisplayItem.java b/src/com/android/gallery3d/ui/DisplayItem.java
index 3038232..50264c4 100644
--- a/src/com/android/gallery3d/ui/DisplayItem.java
+++ b/src/com/android/gallery3d/ui/DisplayItem.java
@@ -18,12 +18,15 @@
 
 public abstract class DisplayItem {
 
-    protected int mWidth;
-    protected int mHeight;
+    protected int mBoxWidth;
+    protected int mBoxHeight;
 
-    protected void setSize(int width, int height) {
-        mWidth = width;
-        mHeight = height;
+    // setBox() specifies the box that the DisplayItem should render into. It
+    // should be called before first render(). It may be called again between
+    // render() calls to change the size of the box.
+    public void setBox(int width, int height) {
+        mBoxWidth = width;
+        mBoxHeight = height;
     }
 
     // returns true if more pass is needed
@@ -31,14 +34,6 @@
 
     public abstract long getIdentity();
 
-    public int getWidth() {
-        return mWidth;
-    }
-
-    public int getHeight() {
-        return mHeight;
-    }
-
     public int getRotation() {
         return 0;
     }
diff --git a/src/com/android/gallery3d/ui/FilmStripView.java b/src/com/android/gallery3d/ui/FilmStripView.java
index a6be2d1..eaf041e 100644
--- a/src/com/android/gallery3d/ui/FilmStripView.java
+++ b/src/com/android/gallery3d/ui/FilmStripView.java
@@ -20,14 +20,16 @@
 import com.android.gallery3d.anim.AlphaAnimation;
 import com.android.gallery3d.app.AlbumDataAdapter;
 import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
 
 import android.content.Context;
 import android.view.MotionEvent;
 import android.view.View.MeasureSpec;
 
-public class FilmStripView extends GLView implements SlotView.Listener,
-        ScrollBarView.Listener, UserInteractionListener {
+public class FilmStripView extends GLView implements ScrollBarView.Listener,
+        UserInteractionListener {
     @SuppressWarnings("unused")
     private static final String TAG = "FilmStripView";
 
@@ -73,10 +75,34 @@
         mGripSize = gripSize;
 
         mStripDrawer = new StripDrawer((Context) activity);
-        mAlbumView = new AlbumView(activity, thumbSize, thumbSize, thumbSize);
+        SlotView.Spec spec = new SlotView.Spec();
+        spec.slotWidth = thumbSize;
+        spec.slotHeight = thumbSize;
+        mAlbumView = new AlbumView(activity, spec, thumbSize);
         mAlbumView.setOverscrollEffect(SlotView.OVERSCROLL_SYSTEM);
         mAlbumView.setSelectionDrawer(mStripDrawer);
-        mAlbumView.setListener(this);
+        mAlbumView.setListener(new SlotView.SimpleListener() {
+            @Override
+            public void onDown(int index) {
+                FilmStripView.this.onDown(index);
+            }
+            @Override
+            public void onUp() {
+                FilmStripView.this.onUp();
+            }
+            @Override
+            public void onSingleTapUp(int slotIndex) {
+                FilmStripView.this.onSingleTapUp(slotIndex);
+            }
+            @Override
+            public void onLongTap(int slotIndex) {
+                FilmStripView.this.onLongTap(slotIndex);
+            }
+            @Override
+            public void onScrollPositionChanged(int position, int total) {
+                FilmStripView.this.onScrollPositionChanged(position, total);
+            }
+        });
         mAlbumView.setUserInteractionListener(this);
         mAlbumDataAdapter = new AlbumDataAdapter(activity, mediaSet);
         addComponent(mAlbumView);
@@ -169,20 +195,32 @@
         super.render(canvas);
     }
 
-    // Called by AlbumView
-    @Override
-    public void onSingleTapUp(int slotIndex) {
+    private void onDown(int index) {
+        MediaItem item = mAlbumDataAdapter.get(index);
+        Path path = (item == null) ? null : item.getPath();
+        mStripDrawer.setPressedPath(path);
+        mAlbumView.invalidate();
+    }
+
+    private void onUp() {
+        mStripDrawer.setPressedPath(null);
+        mAlbumView.invalidate();
+    }
+
+    private void onSingleTapUp(int slotIndex) {
         if (mListener.onSlotSelected(slotIndex)) {
             mAlbumView.setFocusIndex(slotIndex);
         }
     }
 
-    // Called by AlbumView
-    @Override
-    public void onLongTap(int slotIndex) {
+    private void onLongTap(int slotIndex) {
         onSingleTapUp(slotIndex);
     }
 
+    private void onScrollPositionChanged(int position, int total) {
+        mScrollBarView.setContentPosition(position, total);
+    }
+
     // Called by AlbumView
     @Override
     public void onUserInteractionBegin() {
@@ -201,12 +239,6 @@
         mUIListener.onUserInteraction();
     }
 
-    // Called by AlbumView
-    @Override
-    public void onScrollPositionChanged(int position, int total) {
-        mScrollBarView.setContentPosition(position, total);
-    }
-
     // Called by ScrollBarView
     @Override
     public void onScrollBarPositionChanged(int position) {
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
index 387743f..ab0d91b 100644
--- a/src/com/android/gallery3d/ui/GLCanvasImpl.java
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -341,9 +341,17 @@
         // Test whether it has been rotated or flipped, if so, glDrawTexiOES
         // won't work
         if (isMatrixRotatedOrFlipped(mMatrixValues)) {
-            setTextureCoords(0, 0,
-                    (float) texture.getWidth() / texture.getTextureWidth(),
-                    (float) texture.getHeight() / texture.getTextureHeight());
+            if (texture.hasBorder()) {
+                setTextureCoords(
+                        1.0f / texture.getTextureWidth(),
+                        1.0f / texture.getTextureHeight(),
+                        (texture.getWidth() - 1.0f) / texture.getTextureWidth(),
+                        (texture.getHeight() - 1.0f) / texture.getTextureHeight());
+            } else {
+                setTextureCoords(0, 0,
+                        (float) texture.getWidth() / texture.getTextureWidth(),
+                        (float) texture.getHeight() / texture.getTextureHeight());
+            }
             textureRect(x, y, width, height);
         } else {
             // draw the rect from bottom-left to top-right
diff --git a/src/com/android/gallery3d/ui/GLDetailsView.java b/src/com/android/gallery3d/ui/GLDetailsView.java
index cb98d4e..c0542e1 100644
--- a/src/com/android/gallery3d/ui/GLDetailsView.java
+++ b/src/com/android/gallery3d/ui/GLDetailsView.java
@@ -34,6 +34,7 @@
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Rect;
+import android.text.Layout;
 import android.text.format.Formatter;
 import android.view.MotionEvent;
 import android.view.View.MeasureSpec;
@@ -259,7 +260,8 @@
                             context, key), value);
                 }
                 Texture label = MultiLineTexture.newInstance(
-                        value, mMaxDetailLength, FONT_SIZE, FONT_COLOR);
+                        value, mMaxDetailLength, FONT_SIZE, FONT_COLOR,
+                        Layout.Alignment.ALIGN_NORMAL);
                 mItems.add(label);
             }
         }
@@ -274,7 +276,8 @@
 
         public void onAddressAvailable(String address) {
             mItems.set(mLocationIndex, MultiLineTexture.newInstance(
-                    address, mMaxDetailLength, FONT_SIZE, FONT_COLOR));
+                    address, mMaxDetailLength, FONT_SIZE, FONT_COLOR,
+                    Layout.Alignment.ALIGN_NORMAL));
             GLDetailsView.this.invalidate();
         }
     }
diff --git a/src/com/android/gallery3d/ui/GridDrawer.java b/src/com/android/gallery3d/ui/GridDrawer.java
index 54b175c..fbd9e78 100644
--- a/src/com/android/gallery3d/ui/GridDrawer.java
+++ b/src/com/android/gallery3d/ui/GridDrawer.java
@@ -21,28 +21,21 @@
 
 import android.content.Context;
 import android.graphics.Color;
+import android.text.Layout;
 
 public class GridDrawer extends IconDrawer {
-    private final NinePatchTexture mFrame;
-    private final NinePatchTexture mFrameSelected;
-    private final NinePatchTexture mFrameSelectedTop;
-    private final NinePatchTexture mImportBackground;
     private Texture mImportLabel;
     private int mGridWidth;
     private final SelectionManager mSelectionManager;
     private final Context mContext;
-    private final int FONT_SIZE = 14;
-    private final int FONT_COLOR = Color.WHITE;
-    private final int IMPORT_LABEL_PADDING = 10;
+    private final int IMPORT_FONT_SIZE = 14;
+    private final int IMPORT_FONT_COLOR = Color.WHITE;
+    private final int IMPORT_LABEL_MARGIN = 10;
     private boolean mSelectionMode;
 
     public GridDrawer(Context context, SelectionManager selectionManager) {
         super(context);
         mContext = context;
-        mFrame = new NinePatchTexture(context, R.drawable.album_frame);
-        mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
-        mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top);
-        mImportBackground = new NinePatchTexture(context, R.drawable.import_translucent);
         mSelectionManager = selectionManager;
     }
 
@@ -52,9 +45,10 @@
     }
 
     @Override
-    public void draw(GLCanvas canvas, Texture content, int width, int height,
-            int rotation, Path path, int topIndex, int dataSourceType,
-            int mediaType, boolean wantCache, boolean isCaching) {
+    public void draw(GLCanvas canvas, Texture content, int width,
+            int height, int rotation, Path path, int topIndex,
+            int dataSourceType, int mediaType, boolean isPanorama,
+            int labelBackgroundHeight, boolean wantCache, boolean isCaching) {
 
         int x = -width / 2;
         int y = -height / 2;
@@ -70,37 +64,37 @@
             y = -height / 2;
         }
 
-        drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
-
-        NinePatchTexture frame;
-        if (mSelectionMode && mSelectionManager.isItemSelected(path)) {
-            frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected;
-        } else {
-            frame = mFrame;
-        }
-
-        drawFrame(canvas, frame, x, y, width, height);
+        drawMediaTypeOverlay(canvas, mediaType, isPanorama, x, y, width, height,
+                topIndex);
 
         if (topIndex == 0) {
-            ResourceTexture icon = getIcon(dataSourceType);
-            if (icon != null) {
-                IconDimension id = getIconDimension(icon, width, height);
-                if (dataSourceType == DATASOURCE_TYPE_MTP) {
-                    if (mImportLabel == null || mGridWidth != width) {
-                        mGridWidth = width;
-                        mImportLabel = MultiLineTexture.newInstance(
-                                mContext.getString(R.string.click_import),
-                                width - id.width - IMPORT_LABEL_PADDING, FONT_SIZE, FONT_COLOR);
-                    }
-                    int bgHeight = Math.max(id.height, mImportLabel.getHeight());
-                    mImportBackground.setSize(width, bgHeight);
-                    mImportBackground.draw(canvas, x, -y - bgHeight);
-                    mImportLabel.draw(canvas, x + id.width + IMPORT_LABEL_PADDING,
-                            -y - bgHeight + Math.abs(bgHeight - mImportLabel.getHeight()) / 2);
-                }
-                icon.draw(canvas, id.x, id.y, id.width, id.height);
+            drawLabelBackground(canvas, width, height, labelBackgroundHeight);
+            drawIcon(canvas, width, height, dataSourceType);
+            if (dataSourceType == DATASOURCE_TYPE_MTP) {
+                drawImportLabel(canvas, width, height);
             }
         }
+
+        if (mSelectionManager.isPressedPath(path)) {
+            drawPressedFrame(canvas, x, y, width, height);
+        } else if (mSelectionMode && mSelectionManager.isItemSelected(path)) {
+            drawSelectedFrame(canvas, x, y, width, height);
+        }
+    }
+
+    // Draws the "click to import" label at the center of the frame
+    private void drawImportLabel(GLCanvas canvas, int width, int height) {
+        if (mImportLabel == null || mGridWidth != width) {
+            mGridWidth = width;
+            mImportLabel = MultiLineTexture.newInstance(
+                    mContext.getString(R.string.click_import),
+                    width - 2 * IMPORT_LABEL_MARGIN,
+                    IMPORT_FONT_SIZE, IMPORT_FONT_COLOR,
+                    Layout.Alignment.ALIGN_CENTER);
+        }
+        int w = mImportLabel.getWidth();
+        int h = mImportLabel.getHeight();
+        mImportLabel.draw(canvas, -w / 2, -h / 2);
     }
 
     @Override
diff --git a/src/com/android/gallery3d/ui/HighlightDrawer.java b/src/com/android/gallery3d/ui/HighlightDrawer.java
index 9d5868b..d23a00d 100644
--- a/src/com/android/gallery3d/ui/HighlightDrawer.java
+++ b/src/com/android/gallery3d/ui/HighlightDrawer.java
@@ -21,26 +21,23 @@
 import android.content.Context;
 
 public class HighlightDrawer extends IconDrawer {
-    private final NinePatchTexture mFrame;
-    private final NinePatchTexture mFrameSelected;
-    private final NinePatchTexture mFrameSelectedTop;
     private SelectionManager mSelectionManager;
     private Path mHighlightItem;
 
-    public HighlightDrawer(Context context) {
+    public HighlightDrawer(Context context, SelectionManager selectionManager) {
         super(context);
-        mFrame = new NinePatchTexture(context, R.drawable.album_frame);
-        mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
-        mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top);
+        mSelectionManager = selectionManager;
     }
 
     public void setHighlightItem(Path item) {
         mHighlightItem = item;
     }
 
-    public void draw(GLCanvas canvas, Texture content, int width, int height,
-            int rotation, Path path, int topIndex, int dataSourceType,
-            int mediaType, boolean wantCache, boolean isCaching) {
+    @Override
+    public void draw(GLCanvas canvas, Texture content, int width,
+            int height, int rotation, Path path, int topIndex,
+            int dataSourceType, int mediaType, boolean isPanorama,
+            int labelBackgroundHeight, boolean wantCache, boolean isCaching) {
         int x = -width / 2;
         int y = -height / 2;
 
@@ -55,19 +52,18 @@
             y = -height / 2;
         }
 
-        drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
-
-        NinePatchTexture frame;
-        if (path == mHighlightItem) {
-            frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected;
-        } else {
-            frame = mFrame;
-        }
-
-        drawFrame(canvas, frame, x, y, width, height);
+        drawMediaTypeOverlay(canvas, mediaType, isPanorama, x, y, width, height,
+                topIndex);
 
         if (topIndex == 0) {
+            drawLabelBackground(canvas, width, height, labelBackgroundHeight);
             drawIcon(canvas, width, height, dataSourceType);
         }
+
+        if (mSelectionManager.isPressedPath(path)) {
+            drawPressedFrame(canvas, x, y, width, height);
+        } else if (path == mHighlightItem) {
+            drawSelectedFrame(canvas, x, y, width, height);
+        }
     }
 }
diff --git a/src/com/android/gallery3d/ui/IconDrawer.java b/src/com/android/gallery3d/ui/IconDrawer.java
index 91732d3..6ae0157 100644
--- a/src/com/android/gallery3d/ui/IconDrawer.java
+++ b/src/com/android/gallery3d/ui/IconDrawer.java
@@ -21,13 +21,20 @@
 import android.content.Context;
 
 public abstract class IconDrawer extends SelectionDrawer {
-    private final String TAG = "IconDrawer";
+    private static final String TAG = "IconDrawer";
+    private static final int LABEL_BACKGROUND_COLOR = 0x99000000;  // 60% black
+
     private final ResourceTexture mLocalSetIcon;
     private final ResourceTexture mCameraIcon;
     private final ResourceTexture mPicasaIcon;
     private final ResourceTexture mMtpIcon;
+    private final NinePatchTexture mFramePressed;
+    private final NinePatchTexture mFrameSelected;
+    private final NinePatchTexture mDarkStrip;
+    private final ResourceTexture mPanoramaBorder;
     private final Texture mVideoOverlay;
     private final Texture mVideoPlayIcon;
+    private final int mIconSize;
 
     public static class IconDimension {
         int x;
@@ -37,14 +44,18 @@
     }
 
     public IconDrawer(Context context) {
-        mLocalSetIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_folder_holo);
-        mCameraIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_camera_holo);
-        mPicasaIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_picassa_holo);
-        mMtpIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_ptp_holo);
-        mVideoOverlay = new ResourceTexture(context,
-                R.drawable.thumbnail_album_video_overlay_holo);
-        mVideoPlayIcon = new ResourceTexture(context,
-                R.drawable.videooverlay);
+        mLocalSetIcon = new ResourceTexture(context, R.drawable.frame_overlay_gallery_folder);
+        mCameraIcon = new ResourceTexture(context, R.drawable.frame_overlay_gallery_camera);
+        mPicasaIcon = new ResourceTexture(context, R.drawable.frame_overlay_gallery_picasa);
+        mMtpIcon = new ResourceTexture(context, R.drawable.frame_overlay_gallery_ptp);
+        mVideoOverlay = new ResourceTexture(context, R.drawable.ic_video_thumb);
+        mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_gallery_play);
+        mPanoramaBorder = new ResourceTexture(context, R.drawable.ic_pan_thumb);
+        mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed);
+        mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+        mDarkStrip = new NinePatchTexture(context, R.drawable.dark_strip);
+        mIconSize = context.getResources().getDimensionPixelSize(
+                R.dimen.albumset_icon_size);
     }
 
     @Override
@@ -88,24 +99,68 @@
     protected IconDimension getIconDimension(ResourceTexture icon, int width,
             int height) {
         IconDimension id = new IconDimension();
-        float scale = 0.25f * width / icon.getWidth();
-        id.width = (int) (scale * icon.getWidth());
-        id.height = (int) (scale * icon.getHeight());
+        float scale = (float) mIconSize / icon.getWidth();
+        id.width = Math.round(scale * icon.getWidth());
+        id.height = Math.round(scale * icon.getHeight());
         id.x = -width / 2;
-        id.y = height / 2 - id.height;
+        id.y = (height + 1) / 2 - id.height;
         return id;
     }
 
-    protected void drawVideoOverlay(GLCanvas canvas, int mediaType,
-            int x, int y, int width, int height, int topIndex) {
-        if (mediaType != MediaObject.MEDIA_TYPE_VIDEO) return;
-        mVideoOverlay.draw(canvas, x, y, width, height);
+    protected void drawMediaTypeOverlay(GLCanvas canvas, int mediaType,
+            boolean isPanorama, int x, int y, int width, int height,
+            int topIndex) {
+        if (mediaType == MediaObject.MEDIA_TYPE_VIDEO) {
+            drawVideoOverlay(canvas, x, y, width, height, topIndex);
+        }
+        if (isPanorama) {
+            drawPanoramaBorder(canvas, x, y, width, height);
+        }
+    }
+
+    protected void drawVideoOverlay(GLCanvas canvas, int x, int y,
+            int width, int height, int topIndex) {
+        // Scale the video overlay to the height of the thumbnail and put it
+        // on the left side.
+        float scale = (float) height / mVideoOverlay.getHeight();
+        int w = Math.round(scale * mVideoOverlay.getWidth());
+        int h = Math.round(scale * mVideoOverlay.getHeight());
+        mVideoOverlay.draw(canvas, x, y, w, h);
+
         if (topIndex == 0) {
             int side = Math.min(width, height) / 6;
             mVideoPlayIcon.draw(canvas, -side / 2, -side / 2, side, side);
         }
     }
 
+    protected void drawPanoramaBorder(GLCanvas canvas, int x, int y,
+            int width, int height) {
+        float scale = (float) width / mPanoramaBorder.getWidth();
+        int w = Math.round(scale * mPanoramaBorder.getWidth());
+        int h = Math.round(scale * mPanoramaBorder.getHeight());
+        // draw at the top
+        mPanoramaBorder.draw(canvas, x, y, w, h);
+        // draw at the bottom
+        mPanoramaBorder.draw(canvas, x, y + width - h, w, h);
+    }
+
+    protected void drawLabelBackground(GLCanvas canvas, int width, int height,
+            int drawLabelBackground) {
+        int x = -width / 2;
+        int y = (height + 1) / 2 - drawLabelBackground;
+        drawFrame(canvas, mDarkStrip, x, y, width, drawLabelBackground);
+    }
+
+    protected void drawPressedFrame(GLCanvas canvas, int x, int y, int width,
+            int height) {
+        drawFrame(canvas, mFramePressed, x, y, width, height);
+    }
+
+    protected void drawSelectedFrame(GLCanvas canvas, int x, int y, int width,
+            int height) {
+        drawFrame(canvas, mFrameSelected, x, y, width, height);
+    }
+
     @Override
     public void drawFocus(GLCanvas canvas, int width, int height) {
     }
diff --git a/src/com/android/gallery3d/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
index cf1e39e..b1ed249 100644
--- a/src/com/android/gallery3d/ui/ManageCacheDrawer.java
+++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
@@ -23,25 +23,27 @@
 import android.content.Context;
 
 public class ManageCacheDrawer extends IconDrawer {
-    private static final int COLOR_CACHING_BACKGROUND = 0x7F000000;
-    private static final int ICON_SIZE = 36;
-    private final NinePatchTexture mFrame;
     private final ResourceTexture mCheckedItem;
     private final ResourceTexture mUnCheckedItem;
     private final SelectionManager mSelectionManager;
 
     private final ResourceTexture mLocalAlbumIcon;
-    private final StringTexture mCaching;
+    private final StringTexture mCachingText;
 
-    public ManageCacheDrawer(Context context, SelectionManager selectionManager) {
+    private final int mCachePinSize;
+    private final int mCachePinMargin;
+
+    public ManageCacheDrawer(Context context, SelectionManager selectionManager,
+            int cachePinSize, int cachePinMargin) {
         super(context);
-        mFrame = new NinePatchTexture(context, R.drawable.manage_frame);
         mCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_on_holo_dark);
         mUnCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_off_holo_dark);
         mLocalAlbumIcon = new ResourceTexture(context, R.drawable.btn_make_offline_disabled_on_holo_dark);
         String cachingLabel = context.getString(R.string.caching_label);
-        mCaching = StringTexture.newInstance(cachingLabel, 12, 0xffffffff);
+        mCachingText = StringTexture.newInstance(cachingLabel, 12, 0xffffffff);
         mSelectionManager = selectionManager;
+        mCachePinSize = cachePinSize;
+        mCachePinMargin = cachePinMargin;
     }
 
     @Override
@@ -53,12 +55,11 @@
     }
 
     @Override
-    public void draw(GLCanvas canvas, Texture content, int width, int height,
-            int rotation, Path path, int topIndex, int dataSourceType,
-            int mediaType, boolean wantCache, boolean isCaching) {
+    public void draw(GLCanvas canvas, Texture content, int width,
+            int height, int rotation, Path path, int topIndex,
+            int dataSourceType, int mediaType, boolean isPanorama,
+            int labelBackgroundHeight, boolean wantCache, boolean isCaching) {
 
-        boolean selected = mSelectionManager.isItemSelected(path);
-        boolean chooseToCache = wantCache ^ selected;
 
         int x = -width / 2;
         int y = -height / 2;
@@ -74,49 +75,54 @@
             y = -height / 2;
         }
 
-        drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
-
-        drawFrame(canvas, mFrame, x, y, width, height);
+        drawMediaTypeOverlay(canvas, mediaType, isPanorama, x, y, width, height,
+                topIndex);
 
         if (topIndex == 0) {
+            drawLabelBackground(canvas, width, height, labelBackgroundHeight);
             drawIcon(canvas, width, height, dataSourceType);
         }
 
         if (topIndex == 0) {
-            ResourceTexture icon = null;
-            if (isLocal(dataSourceType)) {
-                icon = mLocalAlbumIcon;
-            } else if (chooseToCache) {
-                icon = mCheckedItem;
-            } else {
-                icon = mUnCheckedItem;
-            }
+            drawCachingPin(canvas, path, dataSourceType, isCaching, wantCache,
+                    width, height);
+        }
 
-            int w = ICON_SIZE;
-            int h = ICON_SIZE;
-            x = width / 2 - w / 2;
-            y = -height / 2 - h / 2;
+        if (mSelectionManager.isPressedPath(path)) {
+            drawPressedFrame(canvas, x, y, width, height);
+        }
+    }
 
-            icon.draw(canvas, x, y, w, h);
+    private void drawCachingPin(GLCanvas canvas, Path path, int dataSourceType,
+            boolean isCaching, boolean wantCache, int width, int height) {
+        boolean selected = mSelectionManager.isItemSelected(path);
+        boolean chooseToCache = wantCache ^ selected;
 
-            if (isCaching) {
-                int textWidth = mCaching.getWidth();
-                int textHeight = mCaching.getHeight();
-                x = -textWidth / 2;
-                y = height / 2 - textHeight;
+        ResourceTexture icon = null;
+        if (isLocal(dataSourceType)) {
+            icon = mLocalAlbumIcon;
+        } else if (chooseToCache) {
+            icon = mCheckedItem;
+        } else {
+            icon = mUnCheckedItem;
+        }
 
-                // Leave a few pixels of margin in the background rect.
-                float sideMargin = Utils.clamp(textWidth * 0.1f, 2.0f,
-                        6.0f);
-                float clearance = Utils.clamp(textHeight * 0.1f, 2.0f,
-                        6.0f);
+        int w = mCachePinSize;
+        int h = mCachePinSize;
+        int right = (width + 1) / 2;
+        int bottom = (height + 1) / 2;
+        int x = right - w - mCachePinMargin;
+        int y = bottom - h - mCachePinMargin;
 
-                // Overlay the "Caching" wording at the bottom-center of the content.
-                canvas.fillRect(x - sideMargin, y - clearance,
-                        textWidth + sideMargin * 2, textHeight + clearance,
-                        COLOR_CACHING_BACKGROUND);
-                mCaching.draw(canvas, x, y);
-            }
+        icon.draw(canvas, x, y, w, h);
+
+        if (isCaching) {
+            int textWidth = mCachingText.getWidth();
+            int textHeight = mCachingText.getHeight();
+            // Align the center of the text to the center of the pin icon
+            x = right - mCachePinMargin - (textWidth + mCachePinSize) / 2;
+            y = bottom - textHeight;
+            mCachingText.draw(canvas, x, y);
         }
     }
 
diff --git a/src/com/android/gallery3d/ui/MultiLineTexture.java b/src/com/android/gallery3d/ui/MultiLineTexture.java
index be62d59..b0c3c2b 100644
--- a/src/com/android/gallery3d/ui/MultiLineTexture.java
+++ b/src/com/android/gallery3d/ui/MultiLineTexture.java
@@ -35,10 +35,11 @@
     }
 
     public static MultiLineTexture newInstance(
-            String text, int maxWidth, float textSize, int color) {
+            String text, int maxWidth, float textSize, int color,
+            Layout.Alignment alignment) {
         TextPaint paint = StringTexture.getDefaultPaint(textSize, color);
         Layout layout = new StaticLayout(text, 0, text.length(), paint,
-                maxWidth, Layout.Alignment.ALIGN_NORMAL, 1, 0, true, null, 0);
+                maxWidth, alignment, 1, 0, true, null, 0);
 
         return new MultiLineTexture(layout);
     }
diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java
index 641fc2c..ecc4150 100644
--- a/src/com/android/gallery3d/ui/Paper.java
+++ b/src/com/android/gallery3d/ui/Paper.java
@@ -16,10 +16,14 @@
 
 package com.android.gallery3d.ui;
 
+import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.ui.PositionRepository.Position;
 import com.android.gallery3d.util.GalleryUtils;
 
 import android.opengl.Matrix;
+import android.os.SystemClock;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
 
 import javax.microedition.khronos.opengles.GL11;
 import javax.microedition.khronos.opengles.GL11ExtensionPack;
@@ -28,22 +32,37 @@
 class Paper {
     private static final String TAG = "Paper";
     private static final int ROTATE_FACTOR = 4;
-    private OverscrollAnimation mAnimationLeft = new OverscrollAnimation();
-    private OverscrollAnimation mAnimationRight = new OverscrollAnimation();
+    private EdgeAnimation mAnimationLeft = new EdgeAnimation();
+    private EdgeAnimation mAnimationRight = new EdgeAnimation();
     private int mWidth, mHeight;
     private float[] mMatrix = new float[16];
 
     public void overScroll(float distance) {
+        distance /= mWidth;  // make it relative to width
         if (distance < 0) {
-            mAnimationLeft.scroll(-distance);
+            mAnimationLeft.onPull(-distance);
         } else {
-            mAnimationRight.scroll(distance);
+            mAnimationRight.onPull(distance);
         }
     }
 
-    public boolean advanceAnimation(long currentTimeMillis) {
-        return mAnimationLeft.advanceAnimation(currentTimeMillis)
-            | mAnimationRight.advanceAnimation(currentTimeMillis);
+    public void edgeReached(float velocity) {
+        velocity /= mWidth;  // make it relative to width
+        if (velocity < 0) {
+            mAnimationRight.onAbsorb(-velocity);
+        } else {
+            mAnimationLeft.onAbsorb(velocity);
+        }
+    }
+
+    public void onRelease() {
+        mAnimationLeft.onRelease();
+        mAnimationRight.onRelease();
+    }
+
+    public boolean advanceAnimation() {
+        // Note that we use "|" because we want both animations get updated.
+        return mAnimationLeft.update() | mAnimationRight.update();
     }
 
     public void setSize(int width, int height) {
@@ -56,7 +75,12 @@
         float left = mAnimationLeft.getValue();
         float right = mAnimationRight.getValue();
         float screenX = target.x - scrollX;
-        float t = ((mWidth - screenX) * left - screenX * right) / (mWidth * mWidth);
+        // We linearly interpolate the value [left, right] for the screenX
+        // range int [-1/4, 5/4]*mWidth. So if part of the thumbnail is outside
+        // the screen, we still get some transform.
+        float x = screenX + mWidth / 4;
+        int range = 3 * mWidth / 2;
+        float t = ((range - x) * left - x * right) / range;
         // compress t to the range (-1, 1) by the function
         // f(t) = (1 / (1 + e^-t) - 0.5) * 2
         // then multiply by 90 to make the range (-45, 45)
@@ -71,42 +95,96 @@
     }
 }
 
-class OverscrollAnimation {
-    private static final String TAG = "OverscrollAnimation";
-    private static final long START_ANIMATION = -1;
-    private static final long NO_ANIMATION = -2;
-    private static final long ANIMATION_DURATION = 500;
+// This class follows the structure of frameworks's EdgeEffect class.
+class EdgeAnimation {
+    private static final String TAG = "EdgeAnimation";
 
-    private long mAnimationStartTime = NO_ANIMATION;
-    private float mVelocity;
-    private float mCurrentValue;
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PULL = 1;
+    private static final int STATE_ABSORB = 2;
+    private static final int STATE_RELEASE = 3;
 
-    public void scroll(float distance) {
-        mAnimationStartTime = START_ANIMATION;
-        mCurrentValue += distance;
+    // Time it will take the effect to fully done in ms
+    private static final int ABSORB_TIME = 200;
+    private static final int RELEASE_TIME = 500;
+
+    private static final float VELOCITY_FACTOR = 0.1f;
+
+    private final Interpolator mInterpolator;
+
+    private int mState;
+    private long mAnimationStartTime;
+    private float mValue;
+
+    private float mValueStart;
+    private float mValueFinish;
+    private long mStartTime;
+    private long mDuration;
+
+    public EdgeAnimation() {
+        mInterpolator = new DecelerateInterpolator();
+        mState = STATE_IDLE;
     }
 
-    public boolean advanceAnimation(long currentTimeMillis) {
-        if (mAnimationStartTime == NO_ANIMATION) return false;
-        if (mAnimationStartTime == START_ANIMATION) {
-            mAnimationStartTime = currentTimeMillis;
-            return true;
+    private void startAnimation(float start, float finish, long duration,
+            int newState) {
+        mValueStart = start;
+        mValueFinish = finish;
+        mDuration = duration;
+        mStartTime = now();
+        mState = newState;
+    }
+
+    // The deltaDistance's magnitude is in the range of -1 (no change) to 1.
+    // The value 1 is the full length of the view. Negative values means the
+    // movement is in the opposite direction.
+    public void onPull(float deltaDistance) {
+        if (mState == STATE_ABSORB) return;
+        mValue = Utils.clamp(mValue + deltaDistance, -1.0f, 1.0f);
+        mState = STATE_PULL;
+    }
+
+    public void onRelease() {
+        if (mState == STATE_IDLE || mState == STATE_ABSORB) return;
+        startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+    }
+
+    public void onAbsorb(float velocity) {
+        float finish = Utils.clamp(mValue + velocity * VELOCITY_FACTOR,
+                -1.0f, 1.0f);
+        startAnimation(mValue, finish, ABSORB_TIME, STATE_ABSORB);
+    }
+
+    public boolean update() {
+        if (mState == STATE_IDLE) return false;
+        if (mState == STATE_PULL) return true;
+
+        float t = Utils.clamp((float)(now() - mStartTime) / mDuration, 0.0f, 1.0f);
+        /* Use linear interpolation for absorb, quadratic for others */
+        float interp = (mState == STATE_ABSORB)
+                ? t : mInterpolator.getInterpolation(t);
+
+        mValue = mValueStart + (mValueFinish - mValueStart) * interp;
+
+        if (t >= 1.0f) {
+            switch (mState) {
+                case STATE_ABSORB:
+                    startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+                    break;
+                case STATE_RELEASE:
+                    mState = STATE_IDLE;
+                    break;
+            }
         }
 
-        long deltaTime = currentTimeMillis - mAnimationStartTime;
-        float t = deltaTime / 100f;
-        mCurrentValue *= Math.pow(0.5f, t);
-        mAnimationStartTime = currentTimeMillis;
-
-        if (mCurrentValue < 1) {
-            mAnimationStartTime = NO_ANIMATION;
-            mCurrentValue = 0;
-            return false;
-        }
         return true;
     }
 
     public float getValue() {
-        return mCurrentValue;
+        return mValue;
+    }
+
+    private long now() {
+        return SystemClock.uptimeMillis();
     }
 }
diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java
index 9f19cec..8423518 100644
--- a/src/com/android/gallery3d/ui/ScrollerHelper.java
+++ b/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -58,6 +58,10 @@
         return mScroller.getCurrX();
     }
 
+    public float getCurrVelocity() {
+        return mScroller.getCurrVelocity();
+    }
+
     public void setPosition(int position) {
         mScroller.startScroll(
                 position, 0,    // startX, startY
@@ -77,7 +81,8 @@
                 mOverflingEnabled ? mOverflingDistance : 0, 0);
     }
 
-    public boolean startScroll(int distance, int min, int max) {
+    // Returns the distance that over the scroll limit.
+    public int startScroll(int distance, int min, int max) {
         int currPosition = mScroller.getCurrX();
         int finalPosition = mScroller.getFinalX();
         int newPosition = Utils.clamp(finalPosition + distance, min, max);
@@ -85,9 +90,7 @@
             mScroller.startScroll(
                 currPosition, 0,                    // startX, startY
                 newPosition - currPosition, 0, 0);  // dx, dy, duration
-            return true;
-        } else {
-            return false;
         }
+        return finalPosition + distance - newPosition;
     }
 }
diff --git a/src/com/android/gallery3d/ui/SelectionDrawer.java b/src/com/android/gallery3d/ui/SelectionDrawer.java
index 2655a22..70d8ad5 100644
--- a/src/com/android/gallery3d/ui/SelectionDrawer.java
+++ b/src/com/android/gallery3d/ui/SelectionDrawer.java
@@ -34,15 +34,15 @@
     public abstract void prepareDrawing();
     public abstract void draw(GLCanvas canvas, Texture content,
             int width, int height, int rotation, Path path,
-            int topIndex, int dataSourceType, int mediaType,
-            boolean wantCache, boolean isCaching);
+            int topIndex, int dataSourceType, int mediaType, boolean isPanorama,
+            int labelBackgroundHeight, boolean wantCache, boolean isCaching);
     public abstract void drawFocus(GLCanvas canvas, int width, int height);
 
     public void draw(GLCanvas canvas, Texture content, int width, int height,
-            int rotation, Path path, int mediaType) {
+            int rotation, Path path, int mediaType, boolean isPanorama) {
         draw(canvas, content, width, height, rotation, path, 0,
-                DATASOURCE_TYPE_NOT_CATEGORIZED, mediaType,
-                false, false);
+                DATASOURCE_TYPE_NOT_CATEGORIZED, mediaType, isPanorama,
+                0, false, false);
     }
 
     public static void drawWithRotation(GLCanvas canvas, Texture content,
diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java
index 9599f5b..0ab69d2 100644
--- a/src/com/android/gallery3d/ui/SelectionManager.java
+++ b/src/com/android/gallery3d/ui/SelectionManager.java
@@ -47,6 +47,7 @@
     private boolean mInSelectionMode;
     private boolean mAutoLeave = true;
     private int mTotal;
+    private Path mPressedPath;
 
     public interface SelectionListener {
         public void onSelectionModeChange(int mode);
@@ -141,6 +142,14 @@
         }
     }
 
+    public void setPressedPath(Path path) {
+        mPressedPath = path;
+    }
+
+    public boolean isPressedPath(Path path) {
+        return path != null && path == mPressedPath;
+    }
+
     private static void expandMediaSet(ArrayList<Path> items, MediaSet set) {
         int subCount = set.getSubMediaSetCount();
         for (int i = 0; i < subCount; i++) {
diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java
index a8ca5f2..4b0dc29 100644
--- a/src/com/android/gallery3d/ui/SlotView.java
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -39,12 +39,16 @@
     private static final int INDEX_NONE = -1;
 
     public interface Listener {
+        public void onDown(int index);
+        public void onUp();
         public void onSingleTapUp(int index);
         public void onLongTap(int index);
         public void onScrollPositionChanged(int position, int total);
     }
 
     public static class SimpleListener implements Listener {
+        public void onDown(int index) {}
+        public void onUp() {}
         public void onSingleTapUp(int index) {}
         public void onLongTap(int index) {}
         public void onScrollPositionChanged(int position, int total) {}
@@ -126,8 +130,8 @@
         updateScrollPosition(position, false);
     }
 
-    public void setSlotSize(int slotWidth, int slotHeight) {
-        mLayout.setSlotSize(slotWidth, slotHeight);
+    public void setSlotSpec(Spec spec) {
+        mLayout.setSlotSpec(spec);
     }
 
     @Override
@@ -143,7 +147,15 @@
     @Override
     protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
         if (!changeSize) return;
+
+        // Make sure we are still at a resonable scroll position after the size
+        // is changed (like orientation change). We choose to keep the center
+        // visible slot still visible. This is arbitrary but reasonable.
+        int visibleIndex =
+                (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
         mLayout.setSize(r - l, b - t);
+        makeSlotVisible(visibleIndex);
+
         onLayoutChanged(r - l, b - t);
         if (mOverscrollEffect == OVERSCROLL_3D) {
             mPaper.setSize(r - l, b - t);
@@ -191,6 +203,7 @@
     }
 
     public void putDisplayItem(Position target, Position base, DisplayItem item) {
+        item.setBox(mLayout.getSlotWidth(), mLayout.getSlotHeight());
         ItemEntry entry = new ItemEntry(item, target, base);
         mItemList.insertLast(entry);
         mItems.put(item, entry);
@@ -214,6 +227,10 @@
                 mDownInScrolling = !mScroller.isFinished();
                 mScroller.forceFinished();
                 break;
+            case MotionEvent.ACTION_UP:
+                mPaper.onRelease();
+                invalidate();
+                break;
         }
         return true;
     }
@@ -233,23 +250,34 @@
 
     @Override
     protected void render(GLCanvas canvas) {
-        canvas.save(GLCanvas.SAVE_FLAG_CLIP);
-        canvas.clipRect(0, 0, getWidth(), getHeight());
         super.render(canvas);
 
         long currentTimeMillis = canvas.currentAnimationTimeMillis();
         boolean more = mScroller.advanceAnimation(currentTimeMillis);
-        boolean paperActive = (mOverscrollEffect == OVERSCROLL_3D)
-                && mPaper.advanceAnimation(currentTimeMillis);
+        int oldX = mScrollX;
         updateScrollPosition(mScroller.getPosition(), false);
+
+        boolean paperActive = false;
+        if (mOverscrollEffect == OVERSCROLL_3D) {
+            // Check if an edge is reached and notify mPaper if so.
+            int newX = mScrollX;
+            int limit = mLayout.getScrollLimit();
+            if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) {
+                float v = mScroller.getCurrVelocity();
+                if (newX == limit) v = -v;
+                mPaper.edgeReached(v);
+            }
+            paperActive = mPaper.advanceAnimation();
+        }
+
+        more |= paperActive;
+
         float interpolate = 1f;
         if (mAnimation != null) {
             more |= mAnimation.calculate(currentTimeMillis);
             interpolate = mAnimation.value;
         }
 
-        more |= paperActive;
-
         if (WIDE) {
             canvas.translate(-mScrollX, 0, 0);
         } else {
@@ -291,7 +319,6 @@
             mUIListener.onUserInteractionEnd();
         }
         mMoreAnimation = more;
-        canvas.restore();
     }
 
     private boolean renderItem(GLCanvas canvas, ItemEntry entry,
@@ -350,6 +377,41 @@
         }
     }
 
+    // This Spec class is used to specify the size of each slot in the SlotView.
+    // There are two ways to do it:
+    //
+    // (1) Specify slotWidth and slotHeight: they specify the width and height
+    //     of each slot. The number of rows and the gap between slots will be
+    //     determined automatically.
+    // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number
+    //     of rows in landscape/portrait mode and the gap between slots. The
+    //     width and height of each slot is determined automatically.
+    //
+    // The initial value of -1 means they are not specified.
+    public static class Spec {
+        public int slotWidth = -1;
+        public int slotHeight = -1;
+
+        public int rowsLand = -1;
+        public int rowsPort = -1;
+        public int slotGap = -1;
+
+        static Spec newWithSize(int width, int height) {
+            Spec s = new Spec();
+            s.slotWidth = width;
+            s.slotHeight = height;
+            return s;
+        }
+
+        static Spec newWithRows(int rowsLand, int rowsPort, int slotGap) {
+            Spec s = new Spec();
+            s.rowsLand = rowsLand;
+            s.rowsPort = rowsPort;
+            s.slotGap = slotGap;
+            return s;
+        }
+    }
+
     public static class Layout {
 
         private int mVisibleStart;
@@ -358,6 +420,9 @@
         private int mSlotCount;
         private int mSlotWidth;
         private int mSlotHeight;
+        private int mSlotGap;
+
+        private Spec mSpec;
 
         private int mWidth;
         private int mHeight;
@@ -369,9 +434,8 @@
         private int mVerticalPadding;
         private int mHorizontalPadding;
 
-        public void setSlotSize(int slotWidth, int slotHeight) {
-            mSlotWidth = slotWidth;
-            mSlotHeight = slotHeight;
+        public void setSlotSpec(Spec spec) {
+            mSpec = spec;
         }
 
         public boolean setSlotCount(int slotCount) {
@@ -392,11 +456,19 @@
                 col = index - row * mUnitCount;
             }
 
-            int x = mHorizontalPadding + col * mSlotWidth;
-            int y = mVerticalPadding + row * mSlotHeight;
+            int x = mHorizontalPadding + col * (mSlotWidth + mSlotGap);
+            int y = mVerticalPadding + row * (mSlotHeight + mSlotGap);
             return new Rect(x, y, x + mSlotWidth, y + mSlotHeight);
         }
 
+        public int getSlotWidth() {
+            return mSlotWidth;
+        }
+
+        public int getSlotHeight() {
+            return mSlotHeight;
+        }
+
         public int getContentLength() {
             return mContentLength;
         }
@@ -417,17 +489,19 @@
                 int majorLength, int minorLength,  /* The view width and height */
                 int majorUnitSize, int minorUnitSize,  /* The slot width and height */
                 int[] padding) {
-            int unitCount = minorLength / minorUnitSize;
+            int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap);
             if (unitCount == 0) unitCount = 1;
             mUnitCount = unitCount;
 
             // We put extra padding above and below the column.
             int availableUnits = Math.min(mUnitCount, mSlotCount);
-            padding[0] = (minorLength - availableUnits * minorUnitSize) / 2;
+            int usedMinorLength = availableUnits * minorUnitSize +
+                    (availableUnits - 1) * mSlotGap;
+            padding[0] = (minorLength - usedMinorLength) / 2;
 
             // Then calculate how many columns we need for all slots.
             int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
-            mContentLength = count * majorUnitSize;
+            mContentLength = count * majorUnitSize + (count - 1) * mSlotGap;
 
             // If the content length is less then the screen width, put
             // extra padding in left and right.
@@ -435,6 +509,18 @@
         }
 
         private void initLayoutParameters() {
+            // Initialize mSlotWidth and mSlotHeight from mSpec
+            if (mSpec.slotWidth != -1) {
+                mSlotGap = 0;
+                mSlotWidth = mSpec.slotWidth;
+                mSlotHeight = mSpec.slotHeight;
+            } else {
+                int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort;
+                mSlotGap = mSpec.slotGap;
+                mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows);
+                mSlotWidth = mSlotHeight;
+            }
+
             int[] padding = new int[2];
             if (WIDE) {
                 initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
@@ -458,14 +544,18 @@
             int position = mScrollPosition;
 
             if (WIDE) {
-                int start = Math.max(0, (position / mSlotWidth) * mUnitCount);
-                int end = Math.min(mSlotCount, mUnitCount
-                        * (position + mWidth + mSlotWidth - 1) / mSlotWidth);
+                int startCol = position / (mSlotWidth + mSlotGap);
+                int start = Math.max(0, mUnitCount * startCol);
+                int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) /
+                        (mSlotWidth + mSlotGap);
+                int end = Math.min(mSlotCount, mUnitCount * endCol);
                 setVisibleRange(start, end);
             } else {
-                int start = Math.max(0, mUnitCount * (position / mSlotHeight));
-                int end = Math.min(mSlotCount, mUnitCount
-                        * (position + mHeight + mSlotHeight - 1) / mSlotHeight);
+                int startRow = position / (mSlotHeight + mSlotGap);
+                int start = Math.max(0, mUnitCount * startRow);
+                int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) /
+                        (mSlotHeight + mSlotGap);
+                int end = Math.min(mSlotCount, mUnitCount * endRow);
                 setVisibleRange(start, end);
             }
         }
@@ -495,21 +585,31 @@
         }
 
         public int getSlotIndexByPosition(float x, float y) {
-            float absoluteX = x + (WIDE ? mScrollPosition : 0);
+            int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0);
+            int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition);
+
             absoluteX -= mHorizontalPadding;
-            int columnIdx = (int) (absoluteX + 0.5) / mSlotWidth;
-            if ((absoluteX - mSlotWidth * columnIdx) < 0
-                    || (!WIDE && columnIdx >= mUnitCount)) {
+            absoluteY -= mVerticalPadding;
+
+            int columnIdx = absoluteX / (mSlotWidth + mSlotGap);
+            int rowIdx = absoluteY / (mSlotHeight + mSlotGap);
+
+            if (columnIdx < 0 || (!WIDE && columnIdx >= mUnitCount)) {
                 return INDEX_NONE;
             }
 
-            float absoluteY = y + (WIDE ? 0 : mScrollPosition);
-            absoluteY -= mVerticalPadding;
-            int rowIdx = (int) (absoluteY + 0.5) / mSlotHeight;
-            if (((absoluteY - mSlotHeight * rowIdx) < 0)
-                    || (WIDE && rowIdx >= mUnitCount)) {
+            if (rowIdx < 0 || (WIDE && rowIdx >= mUnitCount)) {
                 return INDEX_NONE;
             }
+
+            if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) {
+                return INDEX_NONE;
+            }
+
+            if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) {
+                return INDEX_NONE;
+            }
+
             int index = WIDE
                     ? (columnIdx * mUnitCount + rowIdx)
                     : (rowIdx * mUnitCount + columnIdx);
@@ -523,12 +623,37 @@
         }
     }
 
-    private class MyGestureListener
-            extends GestureDetector.SimpleOnGestureListener {
+    private class MyGestureListener implements
+            GestureDetector.OnGestureListener {
+        private boolean isDown;
+
+        // We call the listener's onDown() when our onShowPress() is called and
+        // call the listener's onUp() when we receive any further event.
+        @Override
+        public void onShowPress(MotionEvent e) {
+            if (isDown) return;
+            int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+            if (index != INDEX_NONE) {
+                isDown = true;
+                mListener.onDown(index);
+            }
+        }
+
+        private void cancelDown() {
+            if (!isDown) return;
+            isDown = false;
+            mListener.onUp();
+        }
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            return false;
+        }
 
         @Override
         public boolean onFling(MotionEvent e1,
                 MotionEvent e2, float velocityX, float velocityY) {
+            cancelDown();
             int scrollLimit = mLayout.getScrollLimit();
             if (scrollLimit == 0) return false;
             float velocity = WIDE ? velocityX : velocityY;
@@ -541,11 +666,12 @@
         @Override
         public boolean onScroll(MotionEvent e1,
                 MotionEvent e2, float distanceX, float distanceY) {
+            cancelDown();
             float distance = WIDE ? distanceX : distanceY;
-            boolean canMove = mScroller.startScroll(
+            int overDistance = mScroller.startScroll(
                     Math.round(distance), 0, mLayout.getScrollLimit());
-            if (mOverscrollEffect == OVERSCROLL_3D && !canMove) {
-                mPaper.overScroll(distance);
+            if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) {
+                mPaper.overScroll(overDistance);
             }
             invalidate();
             return true;
@@ -553,6 +679,7 @@
 
         @Override
         public boolean onSingleTapUp(MotionEvent e) {
+            cancelDown();
             if (mDownInScrolling) return true;
             int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
             if (index != INDEX_NONE) mListener.onSingleTapUp(index);
@@ -561,6 +688,7 @@
 
         @Override
         public void onLongPress(MotionEvent e) {
+            cancelDown();
             if (mDownInScrolling) return;
             lockRendering();
             try {
diff --git a/src/com/android/gallery3d/ui/StringTexture.java b/src/com/android/gallery3d/ui/StringTexture.java
index 71ab9b3..f576c01 100644
--- a/src/com/android/gallery3d/ui/StringTexture.java
+++ b/src/com/android/gallery3d/ui/StringTexture.java
@@ -56,21 +56,14 @@
     }
 
     public static StringTexture newInstance(
-            String text, String postfix, float textSize, int color,
+            String text, float textSize, int color,
             float lengthLimit, boolean isBold) {
         TextPaint paint = getDefaultPaint(textSize, color);
         if (isBold) {
             paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
         }
-        if (postfix != null) {
-            lengthLimit = Math.max(0,
-                    lengthLimit - paint.measureText(postfix));
-            text = TextUtils.ellipsize(text, paint, lengthLimit,
-                    TextUtils.TruncateAt.END).toString() + postfix;
-        } else {
-            text = TextUtils.ellipsize(
-                    text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();
-        }
+        text = TextUtils.ellipsize(
+                text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();
         return newInstance(text, paint);
     }
 
diff --git a/src/com/android/gallery3d/ui/StripDrawer.java b/src/com/android/gallery3d/ui/StripDrawer.java
index 0910612..5120a0c 100644
--- a/src/com/android/gallery3d/ui/StripDrawer.java
+++ b/src/com/android/gallery3d/ui/StripDrawer.java
@@ -23,27 +23,43 @@
 import android.graphics.Rect;
 
 public class StripDrawer extends SelectionDrawer {
+    private NinePatchTexture mFramePressed;
     private NinePatchTexture mFocusBox;
     private Rect mFocusBoxPadding;
+    private Path mPressedPath;
 
     public StripDrawer(Context context) {
+        mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed);
         mFocusBox = new NinePatchTexture(context, R.drawable.focus_box);
         mFocusBoxPadding = mFocusBox.getPaddings();
     }
 
+    public void setPressedPath(Path path) {
+        mPressedPath = path;
+    }
+
+    private boolean isPressedPath(Path path) {
+        return path != null && path == mPressedPath;
+    }
+
     @Override
     public void prepareDrawing() {
     }
 
     @Override
-    public void draw(GLCanvas canvas, Texture content, int width, int height,
-            int rotation, Path path, int topIndex, int dataSourceType,
-            int mediaType, boolean wantCache, boolean isCaching) {
+    public void draw(GLCanvas canvas, Texture content,
+            int width, int height, int rotation, Path path, int topIndex,
+            int dataSourceType, int mediaType, boolean isPanorama,
+            int labelBackgroundHeight, boolean wantCache, boolean isCaching) {
 
         int x = -width / 2;
         int y = -height / 2;
 
         drawWithRotation(canvas, content, x, y, width, height, rotation);
+
+        if (isPressedPath(path)) {
+            drawFrame(canvas, mFramePressed, x, y, width, height);
+        }
     }
 
     @Override
diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java
index b063824..b2b8cd5 100644
--- a/src/com/android/gallery3d/ui/UploadedTexture.java
+++ b/src/com/android/gallery3d/ui/UploadedTexture.java
@@ -57,9 +57,18 @@
     private static final int UPLOAD_LIMIT = 100;
 
     protected Bitmap mBitmap;
+    private int mBorder;
 
     protected UploadedTexture() {
+        this(false);
+    }
+
+    protected UploadedTexture(boolean hasBorder) {
         super(null, 0, STATE_UNLOADED);
+        if (hasBorder) {
+            setBorder(true);
+            mBorder = 1;
+        }
     }
 
     private static class BorderKey implements Cloneable {
@@ -114,14 +123,14 @@
     private Bitmap getBitmap() {
         if (mBitmap == null) {
             mBitmap = onGetBitmap();
+            int w = mBitmap.getWidth() + mBorder * 2;
+            int h = mBitmap.getHeight() + mBorder * 2;
             if (mWidth == UNSPECIFIED) {
-                setSize(mBitmap.getWidth(), mBitmap.getHeight());
-            } else if (mWidth != mBitmap.getWidth()
-                    || mHeight != mBitmap.getHeight()) {
+                setSize(w, h);
+            } else if (mWidth != w || mHeight != h) {
                 throw new IllegalStateException(String.format(
                         "cannot change size: this = %s, orig = %sx%s, new = %sx%s",
-                        toString(), mWidth, mHeight, mBitmap.getWidth(),
-                        mBitmap.getHeight()));
+                        toString(), mWidth, mHeight, w, h));
             }
         }
         return mBitmap;
@@ -176,8 +185,8 @@
             int format = GLUtils.getInternalFormat(bitmap);
             int type = GLUtils.getType(bitmap);
             canvas.getGLInstance().glBindTexture(GL11.GL_TEXTURE_2D, mId);
-            GLUtils.texSubImage2D(
-                    GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap, format, type);
+            GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0, mBorder, mBorder,
+                    bitmap, format, type);
             freeBitmap();
             mContentValid = true;
         }
@@ -200,14 +209,20 @@
         Bitmap bitmap = getBitmap();
         if (bitmap != null) {
             try {
+                int bWidth = bitmap.getWidth();
+                int bHeight = bitmap.getHeight();
+                int width = bWidth + mBorder * 2;
+                int height = bHeight + mBorder * 2;
+                int texWidth = getTextureWidth();
+                int texHeight = getTextureHeight();
                 // Define a vertically flipped crop rectangle for
                 // OES_draw_texture.
-                int width = bitmap.getWidth();
-                int height = bitmap.getHeight();
-                sCropRect[0] = 0;
-                sCropRect[1] = height;
-                sCropRect[2] = width;
-                sCropRect[3] = -height;
+                // The four values in sCropRect are: left, bottom, width, and
+                // height. Negative value of width or height means flip.
+                sCropRect[0] = mBorder;
+                sCropRect[1] = mBorder + bHeight;
+                sCropRect[2] = bWidth;
+                sCropRect[3] = -bHeight;
 
                 // Upload the bitmap to a new texture.
                 gl.glGenTextures(1, sTextureId, 0);
@@ -223,7 +238,7 @@
                 gl.glTexParameterf(GL11.GL_TEXTURE_2D,
                         GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
 
-                if (width == getTextureWidth() && height == getTextureHeight()) {
+                if (bWidth == texWidth && bHeight == texHeight) {
                     GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bitmap, 0);
                 } else {
                     int format = GLUtils.getInternalFormat(bitmap);
@@ -231,23 +246,35 @@
                     Config config = bitmap.getConfig();
 
                     gl.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format,
-                            getTextureWidth(), getTextureHeight(),
-                            0, format, type, null);
-                    GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap,
-                            format, type);
+                            texWidth, texHeight, 0, format, type, null);
+                    GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0,
+                            mBorder, mBorder, bitmap, format, type);
 
-                    if (width != getTextureWidth()) {
-                        Bitmap line = getBorderLine(true, config, getTextureHeight());
-                        GLUtils.texSubImage2D(
-                                GL11.GL_TEXTURE_2D, 0, width, 0, line, format, type);
+                    if (mBorder > 0) {
+                        // Left border
+                        Bitmap line = getBorderLine(true, config, texHeight);
+                        GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0,
+                                0, 0, line, format, type);
+
+                        // Top border
+                        line = getBorderLine(false, config, texWidth);
+                        GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0,
+                                0, 0, line, format, type);
                     }
 
-                    if (height != getTextureHeight()) {
-                        Bitmap line = getBorderLine(false, config, getTextureWidth());
-                        GLUtils.texSubImage2D(
-                                GL11.GL_TEXTURE_2D, 0, 0, height, line, format, type);
+                    // Right border
+                    if (mBorder + bWidth < texWidth) {
+                        Bitmap line = getBorderLine(true, config, texHeight);
+                        GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0,
+                                mBorder + bWidth, 0, line, format, type);
                     }
 
+                    // Bottom border
+                    if (mBorder + bHeight < texHeight) {
+                        Bitmap line = getBorderLine(false, config, texWidth);
+                        GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0,
+                                0, mBorder + bHeight, line, format, type);
+                    }
                 }
             } finally {
                 freeBitmap();
diff --git a/src/com/android/gallery3d/util/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java
index 9c08dea..6803611 100644
--- a/src/com/android/gallery3d/util/GalleryUtils.java
+++ b/src/com/android/gallery3d/util/GalleryUtils.java
@@ -352,4 +352,11 @@
             output[0] = number;
         }
     }
+
+    public static boolean isPanorama(MediaItem item) {
+        if (item == null) return false;
+        int w = item.getWidth();
+        int h = item.getHeight();
+        return (h > 0 && w / h >= 2);
+    }
 }
diff --git a/tests/src/com/android/gallery3d/data/MockItem.java b/tests/src/com/android/gallery3d/data/MockItem.java
index bd6dcd9..2901979 100644
--- a/tests/src/com/android/gallery3d/data/MockItem.java
+++ b/tests/src/com/android/gallery3d/data/MockItem.java
@@ -40,4 +40,14 @@
     public String getMimeType() {
         return null;
     }
+
+    @Override
+    public int getWidth() {
+        return 0;
+    }
+
+    @Override
+    public int getHeight() {
+        return 0;
+    }
 }