Move database operation to another thread.

Change-Id: I025f428e1df2202dec6344e8b017a752a50e368a
diff --git a/new3d/src/com/android/gallery3d/app/Gallery.java b/new3d/src/com/android/gallery3d/app/Gallery.java
index bf12da3..74d5cc7 100644
--- a/new3d/src/com/android/gallery3d/app/Gallery.java
+++ b/new3d/src/com/android/gallery3d/app/Gallery.java
@@ -27,11 +27,11 @@
 import com.android.gallery3d.data.MediaDbAccessor;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.ui.Compositor;
-import com.android.gallery3d.ui.GLHandler;
 import com.android.gallery3d.ui.GLRootView;
 import com.android.gallery3d.ui.GridSlotAdapter;
 import com.android.gallery3d.ui.MediaSetSlotAdapter;
 import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
 
 public final class Gallery extends Activity implements SlotView.SlotTapListener {
     public static final String REVIEW_ACTION = "com.android.gallery3d.app.REVIEW";
@@ -40,7 +40,7 @@
 
     private static final String TAG = "Gallery";
     private GLRootView mGLRootView;
-    private GLHandler mHandler;
+    private SynchronizedHandler mHandler;
     private Compositor mCompositor;
     private MediaSet mRootSet;
     private SlotView mSlotView;
@@ -48,19 +48,20 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        ImageService.initialize(this);
-
         setContentView(R.layout.main);
         mGLRootView = (GLRootView) findViewById(R.id.gl_root_view);
 
-        mRootSet = MediaDbAccessor.getMediaSets(this);
+        ImageService.initialize(this);
+        MediaDbAccessor.initialize(this, mGLRootView);
+
+        mRootSet = MediaDbAccessor.getInstance().getRootMediaSets();
         mCompositor = new Compositor(this);
         mSlotView = mCompositor.getSlotView();
         mSlotView.setModel(new MediaSetSlotAdapter(this, mRootSet, mSlotView));
         mSlotView.setSlotTapListener(this);
         mGLRootView.setContentPane(mCompositor);
 
-        mHandler = new GLHandler(mGLRootView) {
+        mHandler = new SynchronizedHandler(mGLRootView) {
             @Override
             public void handleMessage(Message message) {
                 switch (message.what) {
@@ -71,7 +72,6 @@
                 }
             }
         };
-
     }
 
     public void onSingleTapUp(int slotIndex) {
diff --git a/new3d/src/com/android/gallery3d/data/BucketMediaSet.java b/new3d/src/com/android/gallery3d/data/BucketMediaSet.java
new file mode 100644
index 0000000..658ff35
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/data/BucketMediaSet.java
@@ -0,0 +1,165 @@
+     // Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.android.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+public class BucketMediaSet implements MediaSet {
+    private static final int MAX_NUM_COVER_ITEMS = 4;
+
+    private static final int MSG_LOAD_DATABASE = 1;
+    private static final int MSG_UPDATE_BUCKET = 2;
+
+    public static final Comparator<BucketMediaSet> sNameComparator = new MyComparator();
+
+    private final MediaDbAccessor mAccessor;
+    private final int mBucketId;
+    private final String mBucketTitle;
+    private final ArrayList<DatabaseMediaItem> mMediaItems =
+            new ArrayList<DatabaseMediaItem>();
+    private ArrayList<DatabaseMediaItem> mLoadBuffer =
+            new ArrayList<DatabaseMediaItem>();
+
+    private final Handler mHandler;
+    private final Handler mMainHandler;
+
+    private MediaSetListener mListener;
+
+    protected void invalidate() {
+        mHandler.sendEmptyMessage(MSG_LOAD_DATABASE);
+    }
+
+    public BucketMediaSet(MediaDbAccessor accessor, int id, String title) {
+        mAccessor = accessor;
+        mBucketId = id;
+        mBucketTitle= title;
+
+        mHandler = new Handler(accessor.getLooper()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_LOAD_DATABASE:
+                        loadMediaItemsFromDatabase();
+                        break;
+                    default: throw new IllegalArgumentException();
+                }
+            }
+        };
+
+        mMainHandler = new SynchronizedHandler(
+                accessor.getUiMonitor(), accessor.getMainLooper()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_UPDATE_BUCKET:
+                        updateContent();
+                        break;
+                    default: throw new IllegalArgumentException();
+                }
+            }
+        };
+
+    }
+
+    public MediaItem[] getCoverMediaItems() {
+        int size = Math.min(MAX_NUM_COVER_ITEMS, mMediaItems.size());
+        MediaItem items[] = new MediaItem[size];
+        for (int i = 0; i < size; ++i) {
+            items[i] = mMediaItems.get(i);
+        }
+        return items;
+    }
+
+    public MediaItem getMediaItem(int index) {
+        return mMediaItems.get(index);
+    }
+
+    public int getMediaItemCount() {
+        return mMediaItems.size();
+    }
+
+    public MediaSet getSubMediaSet(int index) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    public int getSubMediaSetCount() {
+        return 0;
+    }
+
+    public String getTitle() {
+        return mBucketTitle;
+    }
+
+    public int getTotalMediaItemCount() {
+        return mMediaItems.size();
+    }
+
+    public void setContentListener(MediaSetListener listener) {
+        mListener = listener;
+    }
+
+    private void loadMediaItemsFromDatabase() {
+        ArrayList<DatabaseMediaItem> items = new ArrayList<DatabaseMediaItem>();
+        mLoadBuffer = items;
+
+        ContentResolver resolver = mAccessor.getContentResolver();
+
+        Cursor cursor = ImageMediaItem.queryImageInBucket(resolver, mBucketId);
+        try {
+            while (cursor.moveToNext()) {
+                items.add(ImageMediaItem.load(cursor));
+            }
+        } finally {
+            cursor.close();
+        }
+
+        cursor = VideoMediaItem.queryVideoInBucket(resolver, mBucketId);
+        try {
+            while (cursor.moveToNext()) {
+                items.add(VideoMediaItem.load(cursor));
+            }
+        } finally {
+            cursor.close();
+        }
+
+        Collections.sort(items, new Comparator<DatabaseMediaItem>() {
+
+            public int compare(DatabaseMediaItem o1, DatabaseMediaItem o2) {
+                // sort items in descending order based on their taken time.
+                long result = -(o1.mDateTakenInMs - o2.mDateTakenInMs);
+                return result == 0
+                        ? o1.mId - o2.mId
+                        : result > 0 ? 1 : -1;
+            }
+        });
+
+        mMainHandler.sendEmptyMessage(MSG_UPDATE_BUCKET);
+    }
+
+    private void updateContent() {
+        if (mLoadBuffer == null) throw new IllegalArgumentException();
+
+        mMediaItems.clear();
+        mMediaItems.addAll(mLoadBuffer);
+        mLoadBuffer = null;
+
+        if (mListener != null) mListener.onContentChanged();
+    }
+
+    private static class MyComparator implements Comparator<BucketMediaSet> {
+
+        public int compare(BucketMediaSet s1, BucketMediaSet s2) {
+            int result = s1.mBucketTitle.compareTo(s2.mBucketTitle);
+            return result != 0 ? result : s1.mBucketId - s2.mBucketId;
+        }
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/data/DatabaseMediaItem.java b/new3d/src/com/android/gallery3d/data/DatabaseMediaItem.java
index b710daa..9d922df 100644
--- a/new3d/src/com/android/gallery3d/data/DatabaseMediaItem.java
+++ b/new3d/src/com/android/gallery3d/data/DatabaseMediaItem.java
@@ -2,22 +2,17 @@
 
 
 public abstract class DatabaseMediaItem extends AbstractMediaItem {
-    static final int UNCONSTRAINED = -1;
 
-    int mId;
-    String mCaption;
-    String mMimeType;
-    double mLatitude;
-    double mLongitude;
-    long mDateTakenInMs;
-    long mDateAddedInSec;
-    long mDateModifiedInSec;
-    String mFilePath;
-    String mContentUri;
+    protected int mId;
+    protected String mCaption;
+    protected String mMimeType;
+    protected double mLatitude;
+    protected double mLongitude;
+    protected long mDateTakenInMs;
+    protected long mDateAddedInSec;
+    protected long mDateModifiedInSec;
 
-    public String getMediaUri() {
-        return mContentUri;
-    }
+    protected String mFilePath;
 
     public String getTitle() {
         return mCaption;
diff --git a/new3d/src/com/android/gallery3d/data/ImageMediaItem.java b/new3d/src/com/android/gallery3d/data/ImageMediaItem.java
index 44c95a5..1c0e0a3 100644
--- a/new3d/src/com/android/gallery3d/data/ImageMediaItem.java
+++ b/new3d/src/com/android/gallery3d/data/ImageMediaItem.java
@@ -1,9 +1,11 @@
 package com.android.gallery3d.data;
 
 import android.content.ContentResolver;
+import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
 
 import java.io.BufferedInputStream;
 import java.io.FileInputStream;
@@ -17,7 +19,32 @@
     private static final int FULLIMAGE_TARGET_SIZE = 1024;
     private static final int FULLIMAGE_MAX_NUM_PIXELS = 2 * 1024 * 1024;
 
-    public int mRotation;
+    // Must preserve order between these indices and the order of the terms in
+    // PROJECTION_IMAGES.
+    private static final int INDEX_ID = 0;
+    private static final int INDEX_CAPTION = 1;
+    private static final int INDEX_MIME_TYPE = 2;
+    private static final int INDEX_LATITUDE = 3;
+    private static final int INDEX_LONGITUDE = 4;
+    private static final int INDEX_DATE_TAKEN = 5;
+    private static final int INDEX_DATE_ADDED = 6;
+    private static final int INDEX_DATE_MODIFIED = 7;
+    private static final int INDEX_DATA = 8;
+    private static final int INDEX_ORIENTATION = 9;
+
+    private static final String[] PROJECTION_IMAGES =  {
+            ImageColumns._ID,           // 0
+            ImageColumns.TITLE,         // 1
+            ImageColumns.MIME_TYPE,     // 2
+            ImageColumns.LATITUDE,      // 3
+            ImageColumns.LONGITUDE,     // 4
+            ImageColumns.DATE_TAKEN,    // 5
+            ImageColumns.DATE_ADDED,    // 6
+            ImageColumns.DATE_MODIFIED, // 7
+            ImageColumns.DATA,          // 8
+            ImageColumns.ORIENTATION};  // 9
+
+    private int mRotation;
 
     private final BitmapFactory.Options mOptions = new BitmapFactory.Options();
 
@@ -87,4 +114,33 @@
         }
     }
 
+    public static ImageMediaItem load(Cursor cursor) {
+        ImageMediaItem item = new ImageMediaItem();
+
+        item.mId = cursor.getInt(INDEX_ID);
+        item.mCaption = cursor.getString(INDEX_CAPTION);
+        item.mMimeType = cursor.getString(INDEX_MIME_TYPE);
+        item.mLatitude = cursor.getDouble(INDEX_LATITUDE);
+        item.mLongitude = cursor.getDouble(INDEX_LONGITUDE);
+        item.mDateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN);
+        item.mDateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+        item.mDateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+        item.mFilePath = cursor.getString(INDEX_DATA);
+        item.mRotation = cursor.getInt(INDEX_ORIENTATION);
+
+        return item;
+    }
+
+    public static Cursor queryImageInBucket(
+            ContentResolver resolver, int bucketId) {
+        // Build the where clause
+        StringBuilder builder = new StringBuilder(ImageColumns.BUCKET_ID);
+        builder.append(" = ").append(bucketId);
+        String whereClause = builder.toString();
+
+        return resolver.query(
+                Images.Media.EXTERNAL_CONTENT_URI,
+                PROJECTION_IMAGES, whereClause, null, null);
+    }
+
 }
diff --git a/new3d/src/com/android/gallery3d/data/LocalMediaSet.java b/new3d/src/com/android/gallery3d/data/LocalMediaSet.java
index 0a265c4..4e398ab 100644
--- a/new3d/src/com/android/gallery3d/data/LocalMediaSet.java
+++ b/new3d/src/com/android/gallery3d/data/LocalMediaSet.java
@@ -4,7 +4,6 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.Map;
 
 //
@@ -20,15 +19,15 @@
 
     private static final int MAX_NUM_COVERED_ITEMS = 4;
 
-    private ArrayList<LocalMediaSet> mSubMediaSets =
+    private final ArrayList<LocalMediaSet> mSubMediaSets =
             new ArrayList<LocalMediaSet>();
-    private Map<Integer, Integer> mIdsToIndice =
+    private final Map<Integer, Integer> mIdsToIndice =
             new HashMap<Integer, Integer>();
 
-    private ArrayList<MediaItem> mMediaItems = new ArrayList<MediaItem>();
+    private final ArrayList<MediaItem> mMediaItems = new ArrayList<MediaItem>();
 
-    private int mBucketId;
-    private String mTitle;
+    private final int mBucketId;
+    private final String mTitle;
 
     public LocalMediaSet(int bucketId, String title) {
         mBucketId = bucketId;
@@ -127,4 +126,7 @@
             set.printOut();
         }
     }
+
+    public void setContentListener(MediaSetListener listener) {
+    }
 }
diff --git a/new3d/src/com/android/gallery3d/data/MediaDbAccessor.java b/new3d/src/com/android/gallery3d/data/MediaDbAccessor.java
index ef7ca94..4dc88bf 100644
--- a/new3d/src/com/android/gallery3d/data/MediaDbAccessor.java
+++ b/new3d/src/com/android/gallery3d/data/MediaDbAccessor.java
@@ -2,211 +2,61 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.MediaStore.Images;
-import android.provider.MediaStore.Images.ImageColumns;
-import android.provider.MediaStore.Video;
-import android.provider.MediaStore.Video.VideoColumns;
-import android.util.Log;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Process;
+
+import com.android.gallery3d.ui.Util;
 
 public class MediaDbAccessor {
+
     private static final String TAG = "MediaDbAccessor";
 
-    private static final String BASE_CONTENT_STRING_IMAGES =
-        (Images.Media.EXTERNAL_CONTENT_URI).toString() + "/";
-    private static final String BASE_CONTENT_STRING_VIDEOS =
-        (Video.Media.EXTERNAL_CONTENT_URI).toString() + "/";
+    private static MediaDbAccessor sInstance;
 
-    // Must preserve order between these indices and the order of the terms in
-    // BUCKET_PROJECTION_IMAGES, BUCKET_PROJECTION_VIDEOS.
-    // Not using SortedHashMap for efficiency reasons.
-    private static final int BUCKET_ID_INDEX = 0;
-    private static final int BUCKET_NAME_INDEX = 1;
-    private static final String[] BUCKET_PROJECTION_IMAGES = new String[] {
-            ImageColumns.BUCKET_ID, ImageColumns.BUCKET_DISPLAY_NAME };
-    private static final String[] BUCKET_PROJECTION_VIDEOS = new String[] {
-            VideoColumns.BUCKET_ID, VideoColumns.BUCKET_DISPLAY_NAME };
+    private final HandlerThread mThread =
+            new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
 
-    // Must preserve order between these indices and the order of the terms in
-    // INITIAL_PROJECTION_IMAGES and
-    // INITIAL_PROJECTION_VIDEOS.
-    private static final int MEDIA_ID_INDEX = 0;
-    private static final int MEDIA_CAPTION_INDEX = 1;
-    private static final int MEDIA_MIME_TYPE_INDEX = 2;
-    private static final int MEDIA_LATITUDE_INDEX = 3;
-    private static final int MEDIA_LONGITUDE_INDEX = 4;
-    private static final int MEDIA_DATE_TAKEN_INDEX = 5;
-    private static final int MEDIA_DATE_ADDED_INDEX = 6;
-    private static final int MEDIA_DATE_MODIFIED_INDEX = 7;
-    private static final int MEDIA_DATA_INDEX = 8;
-    private static final int MEDIA_ORIENTATION = 9;
-    private static final int DURATION_INDEX = 9;
-    private static final int MEDIA_BUCKET_ID_INDEX = 10;
-    private static final String[] PROJECTION_IMAGES = new String[] {
-            ImageColumns._ID, ImageColumns.TITLE,
-            ImageColumns.MIME_TYPE, ImageColumns.LATITUDE,
-            ImageColumns.LONGITUDE, ImageColumns.DATE_TAKEN,
-            ImageColumns.DATE_ADDED, ImageColumns.DATE_MODIFIED,
-            ImageColumns.DATA, ImageColumns.ORIENTATION,
-            ImageColumns.BUCKET_ID };
+    private final RootMediaSet mRootMediaSet;
+    private final Context mContext;
+    private final Looper mMainLooper;
+    private final Object mUiMonitor;
 
-    private static final String[] PROJECTION_VIDEOS = new String[] {
-            VideoColumns._ID, VideoColumns.TITLE, VideoColumns.MIME_TYPE,
-            VideoColumns.LATITUDE, VideoColumns.LONGITUDE,
-            VideoColumns.DATE_TAKEN, VideoColumns.DATE_ADDED,
-            VideoColumns.DATE_MODIFIED, VideoColumns.DATA,
-            VideoColumns.DURATION, VideoColumns.BUCKET_ID };
+    public MediaDbAccessor(Context context, Object uiMonitor) {
+        mThread.start();
+        mMainLooper = Looper.getMainLooper();
+        mContext = context;
+        mUiMonitor = Util.checkNotNull(uiMonitor);
 
-    private static final String DEFAULT_BUCKET_SORT_ORDER = "upper(" +
-            ImageColumns.BUCKET_DISPLAY_NAME + ") ASC";
-    private static final String DEFAULT_IMAGE_SORT_ORDER =
-            ImageColumns.DATE_TAKEN + " ASC";
-    private static final String DEFAULT_VIDEO_SORT_ORDER =
-            VideoColumns.DATE_TAKEN + " ASC";
-
-    public static LocalMediaSet getMediaSets(Context context) {
-        LocalMediaSet rootSet = new LocalMediaSet(LocalMediaSet.ROOT_SET_ID,
-                "All Albums");
-
-        final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().
-                appendQueryParameter("distinct", "true").build();
-        final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().
-                appendQueryParameter("distinct", "true").build();
-        final ContentResolver cr = context.getContentResolver();
-
-        SortCursor sortCursor = null;
-        try {
-            final Cursor cursorImages = cr.query(uriImages,
-                    BUCKET_PROJECTION_IMAGES, null, null,
-                    DEFAULT_BUCKET_SORT_ORDER);
-            final Cursor cursorVideos = cr.query(uriVideos,
-                    BUCKET_PROJECTION_VIDEOS, null, null,
-                    DEFAULT_BUCKET_SORT_ORDER);
-            Cursor[] cursors = new Cursor[2];
-            cursors[0] = cursorImages;
-            cursors[1] = cursorVideos;
-            sortCursor = new SortCursor(cursors,
-                    ImageColumns.BUCKET_DISPLAY_NAME, SortCursor.TYPE_STRING,
-                    true);
-
-            if (sortCursor.moveToFirst()) {
-                do {
-                    int setId = sortCursor.getInt(BUCKET_ID_INDEX);
-                    LocalMediaSet set =
-                            (LocalMediaSet) rootSet.getSubMediaSetById(setId);
-                    if (set == null) {
-                        set = new LocalMediaSet(setId,
-                                sortCursor.getString(BUCKET_NAME_INDEX));
-                        rootSet.addSubMediaSet(set);
-                    }
-                } while (sortCursor.moveToNext());
-            }
-        } finally {
-            if (sortCursor != null) {
-                sortCursor.close();
-            }
-        }
-        populateMediaItemsForSets(context, rootSet);
-        return rootSet;
+        mRootMediaSet = new RootMediaSet(this);
     }
 
-    private static void populateMediaItemsForSets(Context context,
-            LocalMediaSet rootSet) {
-        final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
-        final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
-        final ContentResolver cr = context.getContentResolver();
-
-        String whereClause = composeWhereClause(rootSet);
-
-        try {
-            final Cursor cursorImages = cr.query(uriImages, PROJECTION_IMAGES,
-                    whereClause, null, DEFAULT_IMAGE_SORT_ORDER);
-            final Cursor cursorVideos = cr.query(uriVideos, PROJECTION_VIDEOS,
-                    whereClause, null, DEFAULT_VIDEO_SORT_ORDER);
-            final Cursor[] cursors = new Cursor[2];
-            cursors[0] = cursorImages;
-            cursors[1] = cursorVideos;
-            final SortCursor sortCursor = new SortCursor(cursors,
-                    ImageColumns.DATE_TAKEN, SortCursor.TYPE_NUMERIC, true);
-            try {
-                if (sortCursor.moveToFirst()) {
-                    do {
-                        final int setId =
-                                sortCursor.getInt(MEDIA_BUCKET_ID_INDEX);
-                        LocalMediaSet set = (LocalMediaSet)
-                                rootSet.getSubMediaSetById(setId);
-                        MediaItem item;
-                        final boolean isVideo =
-                                (sortCursor.getCurrentCursorIndex() == 1);
-                        if (isVideo) {
-                            item = createVideoMediaItemFromCursor(sortCursor,
-                                    BASE_CONTENT_STRING_VIDEOS);
-                        } else {
-                            item = createImageMediaItemFromCursor(sortCursor,
-                                    BASE_CONTENT_STRING_IMAGES);
-                        }
-                        Log.i(TAG, "Item to add in: " + item.getTitle());
-                        set.addMediaItem(item);
-                    } while (sortCursor.moveToNext());
-                }
-            } finally {
-                if (sortCursor != null)
-                    sortCursor.close();
-            }
-        } catch (Exception e) {
-            // If the database operation failed for any reason
-            Log.e(TAG, "Failed to complete the database operation!", e);
-        }
-
+    public static void initialize(Context context, Object uiMonitor) {
+        sInstance = new MediaDbAccessor(context, uiMonitor);
     }
 
-    private static VideoMediaItem createVideoMediaItemFromCursor(Cursor cursor,
-            String baseUri) {
-        VideoMediaItem item = new VideoMediaItem();
-        populateAbstractMediaItemFromCursor(cursor, baseUri, item);
-        item.mDurationInSec = cursor.getInt(DURATION_INDEX);
-        return item;
+    public static MediaDbAccessor getInstance() {
+        if (sInstance == null) throw new IllegalStateException();
+        return sInstance;
     }
 
-    private static ImageMediaItem createImageMediaItemFromCursor(Cursor cursor,
-            String baseUri) {
-         ImageMediaItem item = new ImageMediaItem();
-         populateAbstractMediaItemFromCursor(cursor, baseUri, item);
-         item.mRotation = cursor.getInt(MEDIA_ORIENTATION);
-         return item;
+    public MediaSet getRootMediaSets() {
+        return mRootMediaSet;
     }
 
-    private static void populateAbstractMediaItemFromCursor(Cursor cursor,
-            String baseUri, DatabaseMediaItem item) {
-        item.mId = cursor.getInt(MEDIA_ID_INDEX);
-        item.mCaption = cursor.getString(MEDIA_CAPTION_INDEX);
-        item.mMimeType = cursor.getString(MEDIA_MIME_TYPE_INDEX);
-        item.mLatitude = cursor.getDouble(MEDIA_LATITUDE_INDEX);
-        item.mLongitude = cursor.getDouble(MEDIA_LONGITUDE_INDEX);
-        item.mDateTakenInMs = cursor.getLong(MEDIA_DATE_TAKEN_INDEX);
-        item.mDateAddedInSec = cursor.getLong(MEDIA_DATE_ADDED_INDEX);
-        item.mDateModifiedInSec = cursor.getLong(MEDIA_DATE_MODIFIED_INDEX);
-        item.mFilePath = cursor.getString(MEDIA_DATA_INDEX);
-        if (baseUri != null)
-            item.mContentUri = baseUri + item.mId;
+    public ContentResolver getContentResolver() {
+        return mContext.getContentResolver();
     }
 
-    private static String composeWhereClause(LocalMediaSet rootSet) {
-        int count = rootSet.getSubMediaSetCount();
-        if (count <= 0) {
-            return null;
-        }
-
-        StringBuilder whereString = new StringBuilder(
-                ImageColumns.BUCKET_ID + " in (");
-        for (int i = 0; i < count - 1; ++i) {
-            whereString.append(((LocalMediaSet) rootSet.getSubMediaSet(i))
-                    .getBucketId()).append(",");
-        }
-        whereString.append(((LocalMediaSet) rootSet.getSubMediaSet(count-1))
-                .getBucketId()).append(")");
-        return whereString.toString();
+    public Looper getLooper() {
+        return mThread.getLooper();
     }
 
+    public Looper getMainLooper() {
+        return mMainLooper;
+    }
+
+    public Object getUiMonitor() {
+        return mUiMonitor;
+    }
 }
\ No newline at end of file
diff --git a/new3d/src/com/android/gallery3d/data/MediaItem.java b/new3d/src/com/android/gallery3d/data/MediaItem.java
index 68c3f4c..545cc24 100644
--- a/new3d/src/com/android/gallery3d/data/MediaItem.java
+++ b/new3d/src/com/android/gallery3d/data/MediaItem.java
@@ -18,8 +18,6 @@
         public void onImageCanceled(MediaItem abstractMediaItem, int type);
     }
 
-    public String getMediaUri();
-
     public String getTitle();
 
     public Bitmap getImage(int type);
diff --git a/new3d/src/com/android/gallery3d/data/MediaSet.java b/new3d/src/com/android/gallery3d/data/MediaSet.java
index 3dc8d58..8487a81 100644
--- a/new3d/src/com/android/gallery3d/data/MediaSet.java
+++ b/new3d/src/com/android/gallery3d/data/MediaSet.java
@@ -1,5 +1,6 @@
 package com.android.gallery3d.data;
 
+
 //
 // MediaSet is a directory-like data structure.
 // It contains MediaItems and sub-MediaSets.
@@ -29,4 +30,6 @@
     public String getTitle();
 
     public MediaItem[] getCoverMediaItems();
+
+    public void setContentListener(MediaSetListener listener);
 }
diff --git a/new3d/src/com/android/gallery3d/data/RootMediaSet.java b/new3d/src/com/android/gallery3d/data/RootMediaSet.java
new file mode 100644
index 0000000..cad3290
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/data/RootMediaSet.java
@@ -0,0 +1,184 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.android.gallery3d.data;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.util.Log;
+
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class RootMediaSet implements MediaSet{
+    private static final String TITLE = "RootSet";
+
+    private static final int MSG_LOAD_DATA = 0;
+    private static final int MSG_UPDATE_CONTENT = 0;
+
+    // Must preserve order between these indices and the order of the terms in
+    // BUCKET_PROJECTION_IMAGES, BUCKET_PROJECTION_VIDEOS.
+    // Not using SortedHashMap for efficiency reasons.
+    private static final int BUCKET_ID_INDEX = 0;
+    private static final int BUCKET_NAME_INDEX = 1;
+
+    private static final String[] PROJECTION_IMAGE_BUCKETS = {
+            ImageColumns.BUCKET_ID,
+            ImageColumns.BUCKET_DISPLAY_NAME };
+
+    private static final String[] PROJECTION_VIDEO_BUCKETS = {
+            VideoColumns.BUCKET_ID,
+            VideoColumns.BUCKET_DISPLAY_NAME };
+
+    private final MediaDbAccessor mAccessor;
+    private int mTotalCountCached = -1;
+
+    private final ArrayList<BucketMediaSet>
+            mSubsets = new ArrayList<BucketMediaSet>();
+
+    private HashMap<Integer, String> mLoadBuffer;
+
+    private final Handler mDataHandler;
+    private final Handler mMainHandler;
+
+    private MediaSetListener mListener;
+
+    public RootMediaSet(MediaDbAccessor accessor) {
+        mAccessor = accessor;
+
+        mDataHandler = new Handler(accessor.getLooper()) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_LOAD_DATA:
+                        loadBucketsFromDatabase();
+                        break;
+                    default:
+                        throw new IllegalArgumentException();
+                }
+            }
+        };
+
+        mMainHandler = new SynchronizedHandler(
+                accessor.getUiMonitor(), accessor.getMainLooper()) {
+
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case MSG_UPDATE_CONTENT:
+                        updateContent();
+                        break;
+                    default:
+                        throw new IllegalArgumentException();
+                }
+            }
+        };
+
+        mDataHandler.sendEmptyMessage(MSG_LOAD_DATA);
+    }
+
+    public MediaItem[] getCoverMediaItems() {
+        return new MediaItem[0];
+    }
+
+    public MediaItem getMediaItem(int index) {
+        throw new IndexOutOfBoundsException();
+    }
+
+    public synchronized int getMediaItemCount() {
+        return 0;
+    }
+
+    public synchronized MediaSet getSubMediaSet(int index) {
+        return mSubsets.get(index);
+    }
+
+    public synchronized int getSubMediaSetCount() {
+        return mSubsets.size();
+    }
+
+    public String getTitle() {
+        return TITLE;
+    }
+
+    public int getTotalMediaItemCount() {
+        if (mTotalCountCached >= 0) return mTotalCountCached;
+        int total = 0;
+        for (MediaSet subset : mSubsets) {
+            total += subset.getTotalMediaItemCount();
+        }
+        mTotalCountCached = total;
+        return total;
+    }
+
+    public void setContentListener(MediaSetListener listener) {
+        mListener = listener;
+    }
+
+    private void loadBucketsFromDatabase() {
+        ContentResolver resolver = mAccessor.getContentResolver();
+        HashMap<Integer, String> map = new HashMap<Integer, String>();
+        mLoadBuffer = map;
+
+        Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().
+                appendQueryParameter("distinct", "true").build();
+        Cursor cursor = resolver.query(
+                uriImages, PROJECTION_IMAGE_BUCKETS, null, null, null);
+        if (cursor == null) throw new NullPointerException();
+        try {
+            while (cursor.moveToNext()) {
+                Log.v("Image", cursor.getString(BUCKET_NAME_INDEX));
+                map.put(cursor.getInt(BUCKET_ID_INDEX),
+                        cursor.getString(BUCKET_NAME_INDEX));
+            }
+        } finally {
+            cursor.close();
+        }
+
+        Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().
+                appendQueryParameter("distinct", "true").build();
+        cursor = resolver.query(
+                uriVideos, PROJECTION_VIDEO_BUCKETS, null, null, null);
+        if (cursor == null) throw new NullPointerException();
+        try {
+            while (cursor.moveToNext()) {
+                Log.v("Video", cursor.getString(BUCKET_ID_INDEX));
+                Log.v("Video", cursor.getString(BUCKET_NAME_INDEX));
+                map.put(cursor.getInt(BUCKET_ID_INDEX),
+                        cursor.getString(BUCKET_NAME_INDEX));
+            }
+        } finally {
+            cursor.close();
+        }
+
+        mMainHandler.sendEmptyMessage(MSG_UPDATE_CONTENT);
+    }
+
+    private void updateContent() {
+        HashMap<Integer, String> map = mLoadBuffer;
+        if (map == null) throw new IllegalStateException();
+
+        for (Map.Entry<Integer, String> entry : map.entrySet()) {
+            mSubsets.add(new BucketMediaSet(
+                    mAccessor, entry.getKey(), entry.getValue()));
+        }
+        mLoadBuffer = null;
+
+        Collections.sort(mSubsets, BucketMediaSet.sNameComparator);
+
+        for (BucketMediaSet mediaset : mSubsets) {
+            mediaset.invalidate();
+        }
+        if (mListener != null) mListener.onContentChanged();
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/data/VideoMediaItem.java b/new3d/src/com/android/gallery3d/data/VideoMediaItem.java
index 47a61d3..2985360 100644
--- a/new3d/src/com/android/gallery3d/data/VideoMediaItem.java
+++ b/new3d/src/com/android/gallery3d/data/VideoMediaItem.java
@@ -1,12 +1,41 @@
 package com.android.gallery3d.data;
 
 import android.content.ContentResolver;
+import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.provider.MediaStore.Images;
 import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video.VideoColumns;
 
 public class VideoMediaItem extends DatabaseMediaItem {
     private static final int MICRO_TARGET_PIXELS = 128 * 128;
+
+    // Must preserve order between these indices and the order of the terms in
+    // PROJECTION_VIDEOS.
+    private static final int INDEX_ID = 0;
+    private static final int INDEX_CAPTION = 1;
+    private static final int INDEX_MIME_TYPE = 2;
+    private static final int INDEX_LATITUDE = 3;
+    private static final int INDEX_LONGITUDE = 4;
+    private static final int INDEX_DATE_TAKEN = 5;
+    private static final int INDEX_DATE_ADDED = 6;
+    private static final int INDEX_DATE_MODIFIED = 7;
+    private static final int INDEX_DATA = 8;
+    private static final int INDEX_DURATION = 9;
+
+    private static final String[] PROJECTION_VIDEOS = new String[] {
+            VideoColumns._ID,
+            VideoColumns.TITLE,
+            VideoColumns.MIME_TYPE,
+            VideoColumns.LATITUDE,
+            VideoColumns.LONGITUDE,
+            VideoColumns.DATE_TAKEN,
+            VideoColumns.DATE_ADDED,
+            VideoColumns.DATE_MODIFIED,
+            VideoColumns.DATA,
+            VideoColumns.DURATION};
+
     public int mDurationInSec;
 
     @Override
@@ -33,4 +62,36 @@
                 throw new IllegalArgumentException();
         }
     }
+
+    public static VideoMediaItem load(Cursor cursor) {
+        VideoMediaItem item = new VideoMediaItem();
+
+        item.mId = cursor.getInt(INDEX_ID);
+        item.mCaption = cursor.getString(INDEX_CAPTION);
+        item.mMimeType = cursor.getString(INDEX_MIME_TYPE);
+        item.mLatitude = cursor.getDouble(INDEX_LATITUDE);
+        item.mLongitude = cursor.getDouble(INDEX_LONGITUDE);
+        item.mDateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN);
+        item.mDateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+        item.mDateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+        item.mFilePath = cursor.getString(INDEX_DATA);
+        item.mDurationInSec = cursor.getInt(INDEX_DURATION);
+
+        return item;
+    }
+
+    public static Cursor queryVideoInBucket(
+            ContentResolver resolver, int bucketId) {
+
+        // Build the where clause
+        StringBuilder builder = new StringBuilder(ImageColumns.BUCKET_ID);
+        builder.append(" = ").append(bucketId);
+        String whereClause = builder.toString();
+
+        return resolver.query(
+                Video.Media.EXTERNAL_CONTENT_URI,
+                PROJECTION_VIDEOS, whereClause, null, null);
+    }
+
+
 }
diff --git a/new3d/src/com/android/gallery3d/ui/GLHandler.java b/new3d/src/com/android/gallery3d/ui/GLHandler.java
deleted file mode 100644
index d59f9ef..0000000
--- a/new3d/src/com/android/gallery3d/ui/GLHandler.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.android.gallery3d.ui;
-
-import android.os.Handler;
-import android.os.Message;
-
-public class GLHandler extends Handler {
-
-    private final GLRootView mRootView;
-
-    public GLHandler(GLRootView rootView) {
-        mRootView = rootView;
-    }
-
-    @Override
-    public void dispatchMessage(Message message) {
-        synchronized (mRootView) {
-            super.dispatchMessage(message);
-        }
-    }
-}
diff --git a/new3d/src/com/android/gallery3d/ui/GridSlotAdapter.java b/new3d/src/com/android/gallery3d/ui/GridSlotAdapter.java
index c2c9079..a048ce1 100644
--- a/new3d/src/com/android/gallery3d/ui/GridSlotAdapter.java
+++ b/new3d/src/com/android/gallery3d/ui/GridSlotAdapter.java
@@ -19,17 +19,18 @@
     private static final double EXPECTED_AREA = 150 * 120;
     private static final int SLOT_WIDTH = 162;
     private static final int SLOT_HEIGHT = 132;
-    private static final int INITIAL_CACHE_CAPACITY = 48;
+    private static final int CACHE_CAPACITY = 48;
 
     private final Map<Integer, MyDisplayItem> mItemMap =
-            new HashMap<Integer, MyDisplayItem>(INITIAL_CACHE_CAPACITY);
+            new HashMap<Integer, MyDisplayItem>(CACHE_CAPACITY);
     private final LinkedHashSet<Integer> mLruSlot =
-            new LinkedHashSet<Integer>(INITIAL_CACHE_CAPACITY);
+            new LinkedHashSet<Integer>(CACHE_CAPACITY);
     private final NinePatchTexture mFrame;
 
     private final MediaSet mMediaSet;
     private final Texture mWaitLoadingTexture;
     private final SlotView mSlotView;
+    private boolean mContentInvalidated = false;
 
     public GridSlotAdapter(Context context, MediaSet mediaSet, SlotView slotView) {
         mSlotView = slotView;
@@ -38,11 +39,13 @@
         ColorTexture gray = new ColorTexture(Color.GRAY);
         gray.setSize(64, 48);
         mWaitLoadingTexture = gray;
+        mediaSet.setContentListener(new MyContentListener());
     }
 
     public void putSlot(int slotIndex, int x, int y, DisplayItemPanel panel) {
         MyDisplayItem displayItem = mItemMap.get(slotIndex);
-        if (displayItem == null) {
+
+        if (displayItem == null || mContentInvalidated) {
             MediaItem item = mMediaSet.getMediaItem(slotIndex);
             item.setListener(new MyMediaItemListener(slotIndex));
             switch (item.requestImage(MediaItem.TYPE_MICROTHUMBNAIL)) {
@@ -57,17 +60,18 @@
 
             }
             // Remove an item if the size of mItemsetMap is no less than
-            // INITIAL_CACHE_CAPACITY and there exists a slot in mLruSlot.
-            if (mItemMap.size() >= INITIAL_CACHE_CAPACITY && !mLruSlot.isEmpty()) {
-                Iterator<Integer> iter = mLruSlot.iterator();
-                int index = iter.next();
-                mItemMap.remove(index);
-                mLruSlot.remove(index);
+            // CACHE_CAPACITY and there exists a slot in mLruSlot.
+            Iterator<Integer> iter = mLruSlot.iterator();
+            while (mItemMap.size() >= CACHE_CAPACITY && iter.hasNext()) {
+                mItemMap.remove(iter.next());
+                iter.remove();
             }
             mItemMap.put(slotIndex, displayItem);
-            mLruSlot.remove(slotIndex);
         }
 
+        // Reclaim the slot
+        mLruSlot.remove(slotIndex);
+
         x += getSlotWidth() / 2;
         y += getSlotHeight() / 2;
         panel.putDisplayItem(displayItem, x, y, 0);
@@ -108,6 +112,16 @@
         }
     }
 
+    private void onContentChanged() {
+        mContentInvalidated = true;
+        mSlotView.notifyDataChanged();
+        mContentInvalidated = false;
+
+        for (Integer index : mLruSlot) {
+            mItemMap.remove(index);
+        }
+    }
+
     private static class MyDisplayItem extends DisplayItem {
 
         private Texture mContent;
@@ -157,7 +171,15 @@
     }
 
     public void freeSlot(int index, DisplayItemPanel panel) {
-        panel.removeDisplayItem(mItemMap.get(index));
+        DisplayItem item = mItemMap.get(index);
+        panel.removeDisplayItem(item);
         mLruSlot.add(index);
     }
+
+    private class MyContentListener implements MediaSet.MediaSetListener {
+        public void onContentChanged() {
+            GridSlotAdapter.this.onContentChanged();
+        }
+    }
+
 }
diff --git a/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java b/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java
index 067954d..541c403 100644
--- a/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java
+++ b/new3d/src/com/android/gallery3d/ui/MediaSetSlotAdapter.java
@@ -21,7 +21,8 @@
     private static final int SLOT_WIDTH = 220;
     private static final int SLOT_HEIGHT = 200;
     private static final int MARGIN_TO_SLOTSIDE = 10;
-    private static final int INITIAL_CACHE_CAPACITY = 32;
+    private static final int CACHE_CAPACITY = 32;
+    private static final int INDEX_NONE = -1;
 
     private final NinePatchTexture mFrame;
 
@@ -31,11 +32,14 @@
     private final Texture mWaitLoadingTexture;
 
     private final Map<Integer, MyDisplayItem[]> mItemsetMap =
-            new HashMap<Integer, MyDisplayItem[]>(INITIAL_CACHE_CAPACITY);
+            new HashMap<Integer, MyDisplayItem[]>(CACHE_CAPACITY);
     private final LinkedHashSet<Integer> mLruSlot =
-            new LinkedHashSet<Integer>(INITIAL_CACHE_CAPACITY);
+            new LinkedHashSet<Integer>(CACHE_CAPACITY);
     private final SlotView mSlotView;
 
+    private boolean mContentInvalidated = false;
+    private int mInvalidateIndex = INDEX_NONE;
+
     public MediaSetSlotAdapter(
             Context context, MediaSet rootSet, SlotView view) {
         mRootSet = rootSet;
@@ -44,16 +48,24 @@
         gray.setSize(64, 48);
         mWaitLoadingTexture = gray;
         mSlotView = view;
+
+        rootSet.setContentListener(new MyContentListener());
     }
 
-    public void putSlot(int slotIndex, int x, int y, DisplayItemPanel panel) {
+    public void putSlot(
+            int slotIndex, int x, int y, DisplayItemPanel panel) {
 
         // Get displayItems from mItemsetMap or create them from MediaSet.
         MyDisplayItem[] displayItems = mItemsetMap.get(slotIndex);
-        if (displayItems == null) {
+        if (displayItems == null
+                || mContentInvalidated || mInvalidateIndex == slotIndex) {
             displayItems = createDisplayItems(slotIndex);
+            addSlotToCache(slotIndex, displayItems);
         }
 
+        // Reclaim the slot
+        mLruSlot.remove(slotIndex);
+
         // Put displayItems to the panel.
         Random random = mRandom;
         int left = x + MARGIN_TO_SLOTSIDE;
@@ -72,11 +84,14 @@
             int theta = random.nextInt(31) - 15;
             panel.putDisplayItem(displayItems[i], itemX, y + dy, theta);
         }
-        panel.putDisplayItem(displayItems[0], x, y, 0);
+        if (displayItems.length > 0) {
+            panel.putDisplayItem(displayItems[0], x, y, 0);
+        }
     }
 
     private MyDisplayItem[] createDisplayItems(int slotIndex) {
         MediaSet set = mRootSet.getSubMediaSet(slotIndex);
+        set.setContentListener(new SlotContentListener(slotIndex));
         MediaItem[] items = set.getCoverMediaItems();
 
         MyDisplayItem[] displayItems = new MyDisplayItem[items.length];
@@ -86,8 +101,6 @@
                 case MediaItem.IMAGE_READY:
                     Bitmap bitmap =
                             items[i].getImage(MediaItem.TYPE_MICROTHUMBNAIL);
-                    // TODO: Need to replace BitmapTexture with something else
-                    // whose bitmap is freeable.
                     displayItems[i] = new MyDisplayItem(
                             new BitmapTexture(bitmap), mFrame);
                     break;
@@ -97,7 +110,6 @@
                     break;
             }
         }
-        addSlotToCache(slotIndex, displayItems);
         return displayItems;
     }
 
@@ -105,13 +117,12 @@
         // Remove an itemset if the size of mItemsetMap is no less than
         // INITIAL_CACHE_CAPACITY and there exists a slot in mLruSlot.
         Iterator<Integer> iter = mLruSlot.iterator();
-        for (int i = mItemsetMap.size() - INITIAL_CACHE_CAPACITY;
+        for (int i = mItemsetMap.size() - CACHE_CAPACITY;
                 i >= 0 && iter.hasNext(); --i) {
             mItemsetMap.remove(iter.next());
             iter.remove();
         }
         mItemsetMap.put(slotIndex, displayItems);
-        mLruSlot.remove(slotIndex);
     }
 
     public int getSlotHeight() {
@@ -200,11 +211,65 @@
     }
 
     public void freeSlot(int index, DisplayItemPanel panel) {
-        MyDisplayItem[] displayItems = mItemsetMap.get(index);
+        MyDisplayItem[] displayItems;
+        if (mContentInvalidated) {
+            displayItems = mItemsetMap.remove(index);
+        } else {
+            displayItems = mItemsetMap.get(index);
+            mLruSlot.add(index);
+        }
         for (MyDisplayItem item : displayItems) {
             panel.removeDisplayItem(item);
         }
-        mLruSlot.add(index);
+    }
+
+    private void onContentChanged() {
+        // 1. Remove the original visible itemsets from the display panel.
+        //    These itemsets will be recorded in mLruSlot.
+        // 2. Add the new visible itemsets to the display panel and cache
+        //    (mItemsetMap).
+        mContentInvalidated = true;
+        mSlotView.notifyDataChanged();
+        mContentInvalidated = false;
+
+        // Clean up the cache by removing all itemsets recorded in mLruSlot.
+        for (Integer index : mLruSlot) {
+            mItemsetMap.remove(index);
+        }
+        mLruSlot.clear();
+    }
+
+    private class SlotContentListener implements MediaSet.MediaSetListener {
+        private final int mSlotIndex;
+
+        public SlotContentListener(int slotIndex) {
+            mSlotIndex = slotIndex;
+        }
+
+        public void onContentChanged() {
+            // Update the corresponding itemset to the slot based on whether
+            // the slot is visible or in mLruSlot.
+            // Remove the original corresponding itemset from cache if the
+            // slot was already in mLruSlot. That is the itemset was invisible
+            // and freed before.
+            if (mLruSlot.remove(mSlotIndex)) {
+                mItemsetMap.remove(mSlotIndex);
+            } else {
+                // Refresh the corresponding items in the slot if the slot is
+                // visible. Note that only visible slots are refreshed in
+                // mSlotView.notifySlotInvalidate(mSlotIndex).
+                mInvalidateIndex = mSlotIndex;
+                mSlotView.notifySlotInvalidate(mSlotIndex);
+                mInvalidateIndex = INDEX_NONE;
+            }
+        }
+    }
+
+    private class MyContentListener implements MediaSet.MediaSetListener {
+
+        public void onContentChanged() {
+            MediaSetSlotAdapter.this.onContentChanged();
+        }
     }
 }
 
diff --git a/new3d/src/com/android/gallery3d/ui/Pathbar.java b/new3d/src/com/android/gallery3d/ui/Pathbar.java
index b92f1da..29c73f5 100644
--- a/new3d/src/com/android/gallery3d/ui/Pathbar.java
+++ b/new3d/src/com/android/gallery3d/ui/Pathbar.java
@@ -162,7 +162,7 @@
     @Override
     protected void onAttachToRoot(GLRootView root) {
         super.onAttachToRoot(root);
-        mHandler = new GLHandler(root) {
+        mHandler = new SynchronizedHandler(root) {
             @Override
             public void handleMessage(Message message) {
                 switch (message.what) {
diff --git a/new3d/src/com/android/gallery3d/ui/SlotView.java b/new3d/src/com/android/gallery3d/ui/SlotView.java
index 115f1a9..efd0215 100644
--- a/new3d/src/com/android/gallery3d/ui/SlotView.java
+++ b/new3d/src/com/android/gallery3d/ui/SlotView.java
@@ -1,8 +1,6 @@
 package com.android.gallery3d.ui;
 
 import android.content.Context;
-import android.graphics.Rect;
-import android.util.Log;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.animation.DecelerateInterpolator;
@@ -24,6 +22,7 @@
     private Model mModel;
     private final DisplayItemPanel mPanel;
 
+    private int mSlotCount;
     private int mVerticalGap;
     private int mHorizontalGap;
     private int mSlotWidth;
@@ -58,21 +57,23 @@
 
     public void setModel(Model model) {
         if (model == mModel) return;
-        setVisibleRange(0, 0);
+        if (mModel != null) {
+            // free all the slot in the old model
+            setVisibleRange(0, 0);
+        }
         mModel = model;
-        if (model != null) initializeLayoutParams();
         notifyDataChanged();
     }
 
     private void initializeLayoutParams() {
-        int size = mModel.size();
+        mSlotCount = mModel.size();
         mSlotWidth = mModel.getSlotWidth();
         mSlotHeight = mModel.getSlotHeight();
         int rowCount = (getHeight() - mVerticalGap)
                 / (mVerticalGap + mSlotHeight);
         if (rowCount == 0) rowCount = 1;
         mRowCount = rowCount;
-        mScrollLimit = ((size + rowCount - 1) / rowCount)
+        mScrollLimit = ((mSlotCount + rowCount - 1) / rowCount)
                 * (mHorizontalGap + mSlotWidth)
                 + mHorizontalGap - getWidth();
         if (mScrollLimit < 0) mScrollLimit = 0;
@@ -104,6 +105,10 @@
     }
 
     public void notifyDataChanged() {
+        // free all slots in previous data
+        setVisibleRange(0, 0);
+
+        if (mModel != null) initializeLayoutParams();
         setScrollPosition(0, true);
         notifyDataInvalidate();
     }
@@ -143,7 +148,7 @@
         int rowCount = mRowCount;
         DisplayItemPanel panel = mPanel;
         for (int i = columnIndex * rowCount,
-                n = Math.min(mModel.size(), i + rowCount); i < n; ++i) {
+                n = Math.min(mSlotCount, i + rowCount); i < n; ++i) {
             mModel.freeSlot(i, panel);
         }
     }
@@ -156,17 +161,28 @@
 
         DisplayItemPanel panel = mPanel;
         for (int i = columnIndex * rowCount,
-                n = Math.min(mModel.size(), i + rowCount); i < n; ++i) {
+                n = Math.min(mSlotCount, i + rowCount); i < n; ++i) {
             mModel.putSlot(i, x, y, panel);
             y += rowHeight;
         }
     }
 
+    public void notifySlotInvalidate(int slotIndex) {
+        int columnIndex = slotIndex / mRowCount;
+        if (columnIndex >= mVisibleStart && columnIndex < mVisibleEnd) {
+            mModel.freeSlot(slotIndex, mPanel);
+            int x = columnIndex * (mHorizontalGap + mSlotWidth) + mHorizontalGap;
+            int rowIndex = slotIndex - (columnIndex * mRowCount);
+            int y = mVerticalGap + (mVerticalGap + mSlotHeight) * rowIndex;
+            mModel.putSlot(slotIndex, x, y, mPanel);
+        }
+    }
+
     // start: inclusive, end: exclusive
     private void setVisibleRange(int start, int end) {
         if (start == mVisibleStart && end == mVisibleEnd) return;
         int rowCount = mRowCount;
-        if (start >= mVisibleEnd || end < mVisibleStart) {
+        if (start >= mVisibleEnd || end <= mVisibleStart) {
             for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
                 freeSlotsInColumn(i);
             }
@@ -245,7 +261,7 @@
             return NOT_AT_SLOTPOSITION;
         }
         int index = columnIdx * mRowCount + rowIdx;
-        return index >= mModel.size() ? NOT_AT_SLOTPOSITION : index;
+        return index >= mSlotCount ? NOT_AT_SLOTPOSITION : index;
     }
 
     public interface SlotTapListener {
diff --git a/new3d/src/com/android/gallery3d/ui/SlotViewMockData.java b/new3d/src/com/android/gallery3d/ui/SlotViewMockData.java
index c86c45a..850bf9b 100644
--- a/new3d/src/com/android/gallery3d/ui/SlotViewMockData.java
+++ b/new3d/src/com/android/gallery3d/ui/SlotViewMockData.java
@@ -6,6 +6,7 @@
 import com.android.gallery3d.R;
 
 import java.util.Random;
+import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
 
 public class SlotViewMockData implements SlotView.Model {
     private static final int LENGTH_LIMIT = 150;
@@ -124,4 +125,7 @@
         }
     }
 
+    public ReadLock readLock() {
+        return null;
+    }
 }
diff --git a/new3d/src/com/android/gallery3d/ui/SynchronizedHandler.java b/new3d/src/com/android/gallery3d/ui/SynchronizedHandler.java
new file mode 100644
index 0000000..998997d
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/ui/SynchronizedHandler.java
@@ -0,0 +1,26 @@
+package com.android.gallery3d.ui;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+public class SynchronizedHandler extends Handler {
+
+    private final Object mMonitor;
+
+    public SynchronizedHandler(Object monitor) {
+        mMonitor = Util.checkNotNull(monitor);
+    }
+
+    public SynchronizedHandler(Object monitor, Looper looper) {
+        super(looper);
+        mMonitor = Util.checkNotNull(monitor);
+    }
+
+    @Override
+    public void dispatchMessage(Message message) {
+        synchronized (mMonitor) {
+            super.dispatchMessage(message);
+        }
+    }
+}