Add new interface to MediaSet.

Change-Id: I2339a7b6a6d3ee0dceaca805c5dbd03731a8a7d0
diff --git a/new3d/src/com/android/gallery3d/data/ComboMediaSet.java b/new3d/src/com/android/gallery3d/data/ComboAlbumSet.java
similarity index 65%
rename from new3d/src/com/android/gallery3d/data/ComboMediaSet.java
rename to new3d/src/com/android/gallery3d/data/ComboAlbumSet.java
index 1df9b25..24147dc 100644
--- a/new3d/src/com/android/gallery3d/data/ComboMediaSet.java
+++ b/new3d/src/com/android/gallery3d/data/ComboAlbumSet.java
@@ -16,36 +16,23 @@
 
 package com.android.gallery3d.data;
 
-// Merge multiple media sets into one.
-public class ComboMediaSet extends MediaSet {
-
+// Concatenate multiple media sets into one.
+// This only handles SubMediaSets, not MediaItems. (That's all we need now)
+public class ComboAlbumSet extends MediaSet implements MediaSet.MediaSetListener {
+    private static final String TAG = "ComboAlbumSet";
     private final MediaSet[] mSets;
+    private long mUniqueId;
 
-    public ComboMediaSet(MediaSet ... mediaSets) {
+    public ComboAlbumSet(long uniqueId, MediaSet ... mediaSets) {
+        mUniqueId = uniqueId;
         mSets = mediaSets;
-    }
-
-    public MediaItem[] getCoverMediaItems() {
-        throw new UnsupportedOperationException();
-    }
-
-    public MediaItem getMediaItem(int index) {
         for (MediaSet set : mSets) {
-            int size = set.getMediaItemCount();
-            if (index < size) {
-                return set.getMediaItem(index);
-            }
-            index -= size;
+            set.setContentListener(this);
         }
-        throw new IndexOutOfBoundsException();
     }
 
-    public int getMediaItemCount() {
-        int count = 0;
-        for (MediaSet set : mSets) {
-            count += set.getMediaItemCount();
-        }
-        return count;
+    public long getId() {
+        return mUniqueId;
     }
 
     public MediaSet getSubMediaSet(int index) {
@@ -67,8 +54,8 @@
         return count;
     }
 
-    public String getTitle() {
-        return null;
+    public String getName() {
+        return TAG;
     }
 
     public int getTotalMediaItemCount() {
@@ -79,9 +66,15 @@
         return count;
     }
 
-    public void setContentListener(MediaSetListener listener) {
+    public void reload() {
         for (MediaSet set : mSets) {
-            set.setContentListener(listener);
+            set.reload();
+        }
+    }
+
+    public void onContentChanged() {
+        if (mListener != null) {
+            mListener.onContentChanged();
         }
     }
 }
diff --git a/new3d/src/com/android/gallery3d/data/DataManager.java b/new3d/src/com/android/gallery3d/data/DataManager.java
index 39296e4..9736493 100644
--- a/new3d/src/com/android/gallery3d/data/DataManager.java
+++ b/new3d/src/com/android/gallery3d/data/DataManager.java
@@ -23,6 +23,7 @@
 import android.util.Log;
 
 import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.util.IdentityCache;
 
 import java.io.File;
 import java.io.IOException;
@@ -32,13 +33,42 @@
     private static int PICASA_CACHE_MAX_ENTRIES = 5000;
     private static int PICASA_CACHE_MAX_BYTES = 200 * 1024 * 1024;
     private static String PICASA_CACHE_FILE = "/picasaweb";
+
+    // Below are constants for categories.
+    public static final int ID_LOCAL_IMAGE = 1;
+    public static final int ID_LOCAL_VIDEO = 2;
+    public static final int ID_PICASA_IMAGE = 3;
+
+    public static final int ID_LOCAL_IMAGE_ALBUM = 4;
+    public static final int ID_LOCAL_VIDEO_ALBUM = 5;
+    public static final int ID_PICASA_ALBUM = 6;
+
+    public static final int ID_LOCAL_IMAGE_ALBUM_SET = 7;
+    public static final int ID_LOCAL_VIDEO_ALBUM_SET = 8;
+    public static final int ID_PICASA_ALBUM_SET = 9;
+
+    public static final int ID_COMBO_ALBUM_SET = 10;
+    public static final int ID_MERGE_LOCAL_ALBUM_SET = 11;
+    public static final int ID_MERGE_LOCAL_ALBUM = 12;
+
     private GalleryContext mContext;
     private MediaSet mRootSet;
     private HandlerThread mDataThread;
+    private IdentityCache<Long, MediaItem> mMediaItemCache;
     private BlobCache mPicasaCache = null;
 
     public DataManager(GalleryContext context) {
         mContext = context;
+        mMediaItemCache = new IdentityCache<Long, MediaItem>();
+    }
+
+    public static long makeId(int category, int item) {
+        long result = category;
+        return (result << 32) | item;
+    }
+
+    public static int extractItemId(long id) {
+        return (int) id;
     }
 
     // Return null when we cannot instantiate a BlobCache, e.g.:
@@ -70,10 +100,17 @@
     public MediaSet getRootSet() {
         if (mRootSet == null) {
             PicasaAlbumSet picasaSet = new PicasaAlbumSet(mContext);
-            LocalAlbumSet localSet = new LocalAlbumSet(mContext);
-            picasaSet.invalidate();
+            LocalAlbumSet localImageSet = new LocalAlbumSet(mContext, true);
+            LocalAlbumSet localVideoSet = new LocalAlbumSet(mContext, false);
+            MediaSet localSet = new MergeAlbumSet(
+                    makeId(ID_MERGE_LOCAL_ALBUM_SET, 0),
+                    LocalAlbum.sDateTakenComparator,
+                    localImageSet, localVideoSet);
 
-            mRootSet = new ComboMediaSet(localSet, picasaSet);
+            mRootSet = new ComboAlbumSet(
+                    makeId(ID_COMBO_ALBUM_SET, 0),
+                    localSet, picasaSet);
+            mRootSet.reload();
         }
         return mRootSet;
     }
@@ -82,6 +119,14 @@
         return getRootSet().getSubMediaSet(subSetIndex);
     }
 
+    public MediaItem getFromCache(Long key) {
+        return mMediaItemCache.get(key);
+    }
+
+    public MediaItem putToCache(long key, MediaItem item) {
+        return mMediaItemCache.put(Long.valueOf(key), item);
+    }
+
     public synchronized Looper getDataLooper() {
         if (mDataThread == null ) {
             mDataThread = new HandlerThread(
diff --git a/new3d/src/com/android/gallery3d/data/DatabaseMediaSet.java b/new3d/src/com/android/gallery3d/data/DatabaseMediaSet.java
index e916bed..f9a66c5 100644
--- a/new3d/src/com/android/gallery3d/data/DatabaseMediaSet.java
+++ b/new3d/src/com/android/gallery3d/data/DatabaseMediaSet.java
@@ -16,6 +16,7 @@
 
 package com.android.gallery3d.data;
 
+import android.content.ContentResolver;
 import android.os.Handler;
 import android.os.Message;
 
@@ -23,48 +24,35 @@
 import com.android.gallery3d.ui.SynchronizedHandler;
 import com.android.gallery3d.util.Utils;
 
-import java.util.concurrent.atomic.AtomicInteger;
-
 public abstract class DatabaseMediaSet extends MediaSet {
 
     private static final int MSG_LOAD_DATABASE = 0;
     private static final int MSG_UPDATE_CONTENT = 1;
 
-    private static final int BIT_INVALIDATING = 1;
-    private static final int BIT_PENDING = 2;
-
     protected final Handler mMainHandler;
     protected final Handler mDbHandler;
     protected final GalleryContext mContext;
+    protected final ContentResolver mResolver;
 
-    protected MediaSetListener mListener;
-    private AtomicInteger mState = new AtomicInteger();
+    // How many times do we need to reload: 1 means we are reloading,
+    // 2 means after current reloading, we need to do another one.
+    private int mReloadCount;
 
     protected DatabaseMediaSet(GalleryContext context) {
         mContext = context;
+        mResolver = mContext.getContentResolver();
 
-        mMainHandler = new Handler() {
+        mMainHandler = new SynchronizedHandler(context.getGLRootView()) {
             @Override
             public void handleMessage(Message message) {
                 Utils.Assert(message.what == MSG_UPDATE_CONTENT);
                 onUpdateContent();
                 if (mListener != null) mListener.onContentChanged();
 
-                while (true) {
-                    int s = mState.get();
-
-                    // Either (1) resets the the pending bit and sets the
-                    //            invalidating bit, or
-                    //        (2) resets the state to 0 if the pending bit was
-                    //            originally cleared.
-                    int t = (s & BIT_PENDING) == 0 ? 0 : BIT_INVALIDATING;
-                    if (mState.compareAndSet(s, t)) {
-                        if (t == BIT_INVALIDATING) {
-                            // Case 1: clear the pending bit by loading data
-                            //         from database.
-                            mDbHandler.sendEmptyMessage(MSG_LOAD_DATABASE);
-                        }
-                        break;
+                synchronized (DatabaseMediaSet.this) {
+                    // If we still have pending reload, do it now.
+                    if (--mReloadCount > 0) {
+                        mDbHandler.sendEmptyMessage(MSG_LOAD_DATABASE);
                     }
                 }
             }
@@ -80,28 +68,15 @@
         };
     }
 
-    public void invalidate() {
-        while (true) {
-            int s = mState.get();
-
-            // State is moved either to (1) invalidating, or (2) invalidating and pending.
-            int t = (s & BIT_INVALIDATING) == 0
-                    ? BIT_INVALIDATING
-                    : BIT_INVALIDATING | BIT_PENDING;
-            if (mState.compareAndSet(s, t)) {
-                if (t == BIT_INVALIDATING) {
-                    // Case 1: loading data from database.
-                    mDbHandler.sendEmptyMessage(MSG_LOAD_DATABASE);
-                }
-                break;
-            }
+    public synchronized void reload() {
+        // If we already have reload pending, just return.
+        if (mReloadCount >= 2) return;
+        // If this is the first reload, start it.
+        if (++mReloadCount == 1) {
+            mDbHandler.sendEmptyMessage(MSG_LOAD_DATABASE);
         }
     }
 
-    public void setContentListener(MediaSetListener listener) {
-        mListener = listener;
-    }
-
     abstract protected void onLoadFromDatabase();
     abstract protected void onUpdateContent();
 }
diff --git a/new3d/src/com/android/gallery3d/data/LocalAlbum.java b/new3d/src/com/android/gallery3d/data/LocalAlbum.java
index 17a9e0e..a62e57c 100644
--- a/new3d/src/com/android/gallery3d/data/LocalAlbum.java
+++ b/new3d/src/com/android/gallery3d/data/LocalAlbum.java
@@ -18,117 +18,142 @@
 
 import android.content.ContentResolver;
 import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video.VideoColumns;
 
 import com.android.gallery3d.app.GalleryContext;
 import com.android.gallery3d.util.Utils;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Comparator;
 
-public class LocalAlbum extends DatabaseMediaSet {
-    private static final int MAX_NUM_COVER_ITEMS = 4;
+public class LocalAlbum extends MediaSet {
+    private static final String TAG = "LocalAlbum";
+    private static final String[] COUNT_PROJECTION = { "count(*)" };
+    private final String mWhereClause;
+    private final String mOrderClause;
+    private final Uri mBaseUri;
+    private final String[] mProjection;
+    public static final Comparator<LocalAlbum> sBucketNameComparator =
+            new BucketNameComparator();
+    public static final Comparator<MediaItem> sDateTakenComparator =
+            new DateTakenComparator();
 
-    public static final Comparator<LocalAlbum> sNameComparator = new MyComparator();
-
+    private final GalleryContext mContext;
+    private final ContentResolver mResolver;
     private final int mBucketId;
-    private final String mBucketTitle;
+    private final String mBucketName;
+    private boolean mIsImage;
+    private long mUniqueId;
 
-    private final ArrayList<LocalMediaItem> mMediaItems =
-            new ArrayList<LocalMediaItem>();
-    private ArrayList<LocalMediaItem> mLoadBuffer =
-            new ArrayList<LocalMediaItem>();
+    public LocalAlbum(GalleryContext context, int bucketId, String name, boolean isImage) {
+        mContext = context;
+        mResolver = context.getContentResolver();
+        mBucketId = bucketId;
+        mBucketName = name;
+        mIsImage = isImage;
 
-    public LocalAlbum(GalleryContext context, int id, String title) {
-        super(context);
-        mBucketId = id;
-        mBucketTitle= title;
-    }
-
-    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);
+        if (isImage) {
+            mWhereClause = ImageColumns.BUCKET_ID + "=?";
+            mOrderClause = ImageColumns.DATE_TAKEN + " DESC, "
+                    + ImageColumns._ID + " ASC";
+            mBaseUri = Images.Media.EXTERNAL_CONTENT_URI;
+            mProjection = LocalImage.PROJECTION;
+            mUniqueId = DataManager.makeId(
+                    DataManager.ID_LOCAL_IMAGE_ALBUM, bucketId);
+        } else {
+            mWhereClause = VideoColumns.BUCKET_ID + "=?";
+            mOrderClause = VideoColumns.DATE_TAKEN + " DESC, "
+                    + VideoColumns._ID + " ASC";
+            mBaseUri = Video.Media.EXTERNAL_CONTENT_URI;
+            mProjection = LocalVideo.PROJECTION;
+            mUniqueId = DataManager.makeId(
+                    DataManager.ID_LOCAL_VIDEO_ALBUM, bucketId);
         }
-        return items;
     }
 
-    public MediaItem getMediaItem(int index) {
-        return mMediaItems.get(index);
+    public long getId() {
+        return mUniqueId;
+    }
+
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        ImageService imageService = mContext.getImageService();
+        DataManager dataManager = mContext.getDataManager();
+
+        Uri uri = mBaseUri.buildUpon()
+                .appendQueryParameter("limit", start + "," + count).build();
+        ArrayList<MediaItem> list = new ArrayList<MediaItem>();
+        Cursor cursor = mResolver.query(
+                uri, mProjection, mWhereClause,
+                new String[]{String.valueOf(mBucketId)},
+                mOrderClause);
+
+        try {
+            while (cursor.moveToNext()) {
+                if (mIsImage) {
+                    list.add(LocalImage.load(imageService, cursor, dataManager));
+                } else {
+                    list.add(LocalVideo.load(imageService, cursor, dataManager));
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+        return list;
     }
 
     public int getMediaItemCount() {
-        return mMediaItems.size();
+        Cursor cursor = mResolver.query(
+                mBaseUri, COUNT_PROJECTION, mWhereClause,
+                new String[]{String.valueOf(mBucketId)}, null);
+        try {
+            Utils.Assert(cursor.moveToNext());
+            return cursor.getInt(0);
+        } finally {
+            cursor.close();
+        }
     }
 
-    public MediaSet getSubMediaSet(int index) {
-        throw new IndexOutOfBoundsException();
-    }
-
-    public int getSubMediaSetCount() {
-        return 0;
-    }
-
-    public String getTitle() {
-        return mBucketTitle;
+    public String getName() {
+        return mBucketName;
     }
 
     public int getTotalMediaItemCount() {
-        return mMediaItems.size();
+        return getMediaItemCount();
     }
 
-    @Override
-    protected void onLoadFromDatabase() {
-        ArrayList<LocalMediaItem> items = new ArrayList<LocalMediaItem>();
-        mLoadBuffer = items;
-
-        ContentResolver resolver = mContext.getContentResolver();
-        ImageService imageService = mContext.getImageService();
-
-        Cursor cursor = LocalImage.queryImageInBucket(resolver, mBucketId);
-        try {
-            while (cursor.moveToNext()) {
-                items.add(LocalImage.load(imageService, cursor));
-            }
-        } finally {
-            cursor.close();
-        }
-
-        cursor = LocalVideo.queryVideoInBucket(resolver, mBucketId);
-        try {
-            while (cursor.moveToNext()) {
-                items.add(LocalVideo.load(imageService, cursor));
-            }
-        } finally {
-            cursor.close();
-        }
-
-        Collections.sort(items, new Comparator<LocalMediaItem>() {
-
-            public int compare(LocalMediaItem o1, LocalMediaItem 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;
-            }
-        });
-    }
-
-    @Override
-    protected void onUpdateContent() {
-        Utils.Assert(mLoadBuffer != null);
-        mMediaItems.clear();
-        mMediaItems.addAll(mLoadBuffer);
-        mLoadBuffer = null;
-    }
-
-    private static class MyComparator implements Comparator<LocalAlbum> {
-
+    private static class BucketNameComparator implements Comparator<LocalAlbum> {
         public int compare(LocalAlbum s1, LocalAlbum s2) {
-            int result = s1.mBucketTitle.compareTo(s2.mBucketTitle);
-            return result != 0 ? result : s1.mBucketId - s2.mBucketId;
+            int result = s1.mBucketName.compareTo(s2.mBucketName);
+            if (result != 0) return result;
+            if (s1.mBucketId > s2.mBucketId) {
+                return 1;
+            } else if (s1.mBucketId < s2.mBucketId) {
+                return -1;
+            } else {
+                return 0;
+            }
         }
     }
+
+    private static class DateTakenComparator implements Comparator<MediaItem> {
+        public int compare(MediaItem item1, MediaItem item2) {
+            LocalMediaItem s1 = (LocalMediaItem) item1;
+            LocalMediaItem s2 = (LocalMediaItem) item2;
+            if (s1.mDateTakenInMs > s2.mDateTakenInMs) {
+                return -1;
+            } else if (s1.mDateTakenInMs < s2.mDateTakenInMs) {
+                return 1;
+            } else {
+                return 0;
+            }
+        }
+    }
+
+    public void reload() {
+        // do nothing
+    }
 }
diff --git a/new3d/src/com/android/gallery3d/data/LocalAlbumSet.java b/new3d/src/com/android/gallery3d/data/LocalAlbumSet.java
index 5ae40f3..7cab3f3 100644
--- a/new3d/src/com/android/gallery3d/data/LocalAlbumSet.java
+++ b/new3d/src/com/android/gallery3d/data/LocalAlbumSet.java
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.data;
 
-import android.content.ContentResolver;
 import android.database.Cursor;
 import android.net.Uri;
 import android.provider.MediaStore.Images;
@@ -32,11 +31,8 @@
 import java.util.Map;
 
 public class LocalAlbumSet extends DatabaseMediaSet {
-    private static final String TITLE = "RootSet";
+    private static final String TAG = "LocalAlbumSet";
 
-    // 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;
 
@@ -48,77 +44,61 @@
             VideoColumns.BUCKET_ID,
             VideoColumns.BUCKET_DISPLAY_NAME };
 
-    private int mTotalCountCached = -1;
+    private final String[] mProjection;
+    private final Uri mBaseUri;
 
-    private final ArrayList<LocalAlbum>
-            mSubsets = new ArrayList<LocalAlbum>();
-
+    private boolean mIsImage;
+    private long mUniqueId;
+    private final ArrayList<LocalAlbum> mAlbums = new ArrayList<LocalAlbum>();
     private HashMap<Integer, String> mLoadBuffer;
 
-    public LocalAlbumSet(GalleryContext context) {
+    public LocalAlbumSet(GalleryContext context, boolean isImage) {
         super(context);
-        invalidate();
+        mIsImage = isImage;
+        if (isImage) {
+            mProjection = PROJECTION_IMAGE_BUCKETS;
+            mBaseUri = Images.Media.EXTERNAL_CONTENT_URI;
+            mUniqueId = DataManager.makeId(DataManager.ID_LOCAL_IMAGE_ALBUM_SET, 0);
+        } else {
+            mProjection = PROJECTION_VIDEO_BUCKETS;
+            mBaseUri = Video.Media.EXTERNAL_CONTENT_URI;
+            mUniqueId = DataManager.makeId(DataManager.ID_LOCAL_VIDEO_ALBUM_SET, 0);
+        }
     }
 
-    public MediaItem[] getCoverMediaItems() {
-        return new MediaItem[0];
-    }
-
-    public MediaItem getMediaItem(int index) {
-        throw new IndexOutOfBoundsException();
-    }
-
-    public synchronized int getMediaItemCount() {
-        return 0;
+    public long getId() {
+        return mUniqueId;
     }
 
     public synchronized MediaSet getSubMediaSet(int index) {
-        return mSubsets.get(index);
+        return mAlbums.get(index);
     }
 
     public synchronized int getSubMediaSetCount() {
-        return mSubsets.size();
+        return mAlbums.size();
     }
 
-    public String getTitle() {
-        return TITLE;
+    public String getName() {
+        return TAG;
     }
 
     public int getTotalMediaItemCount() {
-        if (mTotalCountCached >= 0) return mTotalCountCached;
         int total = 0;
-        for (MediaSet subset : mSubsets) {
-            total += subset.getTotalMediaItemCount();
+        for (MediaSet album : mAlbums) {
+            total += album.getTotalMediaItemCount();
         }
-        mTotalCountCached = total;
         return total;
     }
 
     @Override
     protected void onLoadFromDatabase() {
-
-        ContentResolver resolver = mContext.getContentResolver();
         HashMap<Integer, String> map = new HashMap<Integer, String>();
         mLoadBuffer = map;
 
-        Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().
+        Uri uri = mBaseUri.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()) {
-                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);
+        Cursor cursor = mResolver.query(
+                uri, mProjection, null, null, null);
         if (cursor == null) throw new NullPointerException();
         try {
             while (cursor.moveToNext()) {
@@ -135,17 +115,12 @@
         HashMap<Integer, String> map = mLoadBuffer;
         if (map == null) throw new IllegalStateException();
 
-        GalleryContext context = mContext;
         for (Map.Entry<Integer, String> entry : map.entrySet()) {
-            mSubsets.add(new LocalAlbum(
-                    context, entry.getKey(), entry.getValue()));
+            mAlbums.add(new LocalAlbum(
+                    mContext, entry.getKey(), entry.getValue(), mIsImage));
         }
         mLoadBuffer = null;
 
-        Collections.sort(mSubsets, LocalAlbum.sNameComparator);
-
-        for (LocalAlbum mediaset : mSubsets) {
-            mediaset.invalidate();
-        }
+        Collections.sort(mAlbums, LocalAlbum.sBucketNameComparator);
     }
 }
diff --git a/new3d/src/com/android/gallery3d/data/LocalImage.java b/new3d/src/com/android/gallery3d/data/LocalImage.java
index 8f9636a..5481658 100644
--- a/new3d/src/com/android/gallery3d/data/LocalImage.java
+++ b/new3d/src/com/android/gallery3d/data/LocalImage.java
@@ -52,7 +52,7 @@
     private static final int INDEX_DATA = 8;
     private static final int INDEX_ORIENTATION = 9;
 
-    private static final String[] PROJECTION_IMAGES =  {
+    static final String[] PROJECTION =  {
             ImageColumns._ID,           // 0
             ImageColumns.TITLE,         // 1
             ImageColumns.MIME_TYPE,     // 2
@@ -66,12 +66,17 @@
 
     private final BitmapFactory.Options mOptions = new BitmapFactory.Options();
 
+    private long mUniqueId;
     private int mRotation;
 
     protected LocalImage(ImageService imageService) {
         super(imageService);
     }
 
+    public long getUniqueId() {
+        return mUniqueId;
+    }
+
     protected Bitmap decodeImage(String path) throws IOException {
         // TODO: need to figure out why simply setting JPEG_MARK_POSITION doesn't work!
         BufferedInputStream bis = new BufferedInputStream(
@@ -140,10 +145,17 @@
         }
     }
 
-    public static LocalImage load(ImageService imageService, Cursor cursor) {
-        LocalImage item = new LocalImage(imageService);
+    public static LocalImage load(ImageService imageService, Cursor cursor,
+            DataManager dataManager) {
+        int itemId = cursor.getInt(INDEX_ID);
+        long uniqueId = DataManager.makeId(DataManager.ID_LOCAL_IMAGE, itemId);
+        LocalImage item = (LocalImage) dataManager.getFromCache(uniqueId);
+        if (item != null) return item;
 
-        item.mId = cursor.getInt(INDEX_ID);
+        item = new LocalImage(imageService);
+        dataManager.putToCache(uniqueId, item);
+
+        item.mId = itemId;
         item.mCaption = cursor.getString(INDEX_CAPTION);
         item.mMimeType = cursor.getString(INDEX_MIME_TYPE);
         item.mLatitude = cursor.getDouble(INDEX_LATITUDE);
@@ -153,20 +165,8 @@
         item.mDateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
         item.mFilePath = cursor.getString(INDEX_DATA);
         item.mRotation = cursor.getInt(INDEX_ORIENTATION);
+        item.mUniqueId = uniqueId;
 
         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/LocalMediaItem.java b/new3d/src/com/android/gallery3d/data/LocalMediaItem.java
index 56ba886..3c1a0ca 100644
--- a/new3d/src/com/android/gallery3d/data/LocalMediaItem.java
+++ b/new3d/src/com/android/gallery3d/data/LocalMediaItem.java
@@ -29,7 +29,7 @@
 //
 public abstract class LocalMediaItem extends MediaItem {
 
-    private static final String TAG = LocalMediaItem.class.getSimpleName();
+    private static final String TAG = "LocalMediaItem";
 
     // database fields
     protected int mId;
@@ -47,17 +47,12 @@
 
     protected final ImageService mImageService;
 
-    @SuppressWarnings("unchecked")
     protected LocalMediaItem(ImageService imageService) {
         mImageService = imageService;
         mFutureBitmaps = new MyFuture[TYPE_COUNT];
         mRequestId = new int[TYPE_COUNT];
     }
 
-    public String getTitle() {
-        return mCaption;
-    }
-
     public synchronized Future<Bitmap>
             requestImage(int type, FutureListener<? super Bitmap> listener) {
         if (mFutureBitmaps[type] != null) {
@@ -111,5 +106,4 @@
             cancelImageRequest(mSizeType);
         }
     }
-
 }
diff --git a/new3d/src/com/android/gallery3d/data/LocalVideo.java b/new3d/src/com/android/gallery3d/data/LocalVideo.java
index 9bebcb4..2c317e2 100644
--- a/new3d/src/com/android/gallery3d/data/LocalVideo.java
+++ b/new3d/src/com/android/gallery3d/data/LocalVideo.java
@@ -21,9 +21,7 @@
 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 LocalVideo extends LocalMediaItem {
@@ -31,7 +29,7 @@
     private static final int MICRO_TARGET_PIXELS = 128 * 128;
 
     // Must preserve order between these indices and the order of the terms in
-    // PROJECTION_VIDEOS.
+    // PROJECTION.
     private static final int INDEX_ID = 0;
     private static final int INDEX_CAPTION = 1;
     private static final int INDEX_MIME_TYPE = 2;
@@ -43,7 +41,7 @@
     private static final int INDEX_DATA = 8;
     private static final int INDEX_DURATION = 9;
 
-    private static final String[] PROJECTION_VIDEOS = new String[] {
+    static final String[] PROJECTION = new String[] {
             VideoColumns._ID,
             VideoColumns.TITLE,
             VideoColumns.MIME_TYPE,
@@ -55,12 +53,17 @@
             VideoColumns.DATA,
             VideoColumns.DURATION};
 
+    private long mUniqueId;
     public int mDurationInSec;
 
     protected LocalVideo(ImageService imageService) {
         super(imageService);
     }
 
+    public long getUniqueId() {
+        return mUniqueId;
+    }
+
     @Override
     protected void cancelImageGeneration(ContentResolver resolver, int type) {
         Video.Thumbnails.cancelThumbnailRequest(resolver, mId);
@@ -74,10 +77,10 @@
             case TYPE_FULL_IMAGE:
             case TYPE_THUMBNAIL:
                 return Video.Thumbnails.getThumbnail(
-                        resolver, mId, Images.Thumbnails.MINI_KIND, null);
+                        resolver, mId, Video.Thumbnails.MINI_KIND, null);
             case TYPE_MICROTHUMBNAIL:
                 Bitmap bitmap = Video.Thumbnails.getThumbnail(
-                        resolver, mId, Images.Thumbnails.MINI_KIND, null);
+                        resolver, mId, Video.Thumbnails.MINI_KIND, null);
                 return bitmap == null
                         ? null
                         : Utils.resize(bitmap, MICRO_TARGET_PIXELS);
@@ -86,8 +89,15 @@
         }
     }
 
-    public static LocalVideo load(ImageService imageService, Cursor cursor) {
-        LocalVideo item = new LocalVideo(imageService);
+    public static LocalVideo load(ImageService imageService, Cursor cursor,
+            DataManager dataManager) {
+        int itemId = cursor.getInt(INDEX_ID);
+        long uniqueId = DataManager.makeId(DataManager.ID_LOCAL_VIDEO, itemId);
+        LocalVideo item = (LocalVideo) dataManager.getFromCache(uniqueId);
+        if (item != null) return item;
+
+        item = new LocalVideo(imageService);
+        dataManager.putToCache(uniqueId, item);
 
         item.mId = cursor.getInt(INDEX_ID);
         item.mCaption = cursor.getString(INDEX_CAPTION);
@@ -99,22 +109,8 @@
         item.mDateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
         item.mFilePath = cursor.getString(INDEX_DATA);
         item.mDurationInSec = cursor.getInt(INDEX_DURATION);
+        item.mUniqueId = uniqueId;
 
         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/data/MediaItem.java b/new3d/src/com/android/gallery3d/data/MediaItem.java
index 703cf16..d4a4d6b 100644
--- a/new3d/src/com/android/gallery3d/data/MediaItem.java
+++ b/new3d/src/com/android/gallery3d/data/MediaItem.java
@@ -31,8 +31,7 @@
     public static final int IMAGE_WAIT = 1;
     public static final int IMAGE_ERROR = -1;
 
-    public abstract String getTitle();
-
+    public abstract long getUniqueId();
     public abstract Future<Bitmap>
             requestImage(int type, FutureListener<? super Bitmap> listener);
 }
diff --git a/new3d/src/com/android/gallery3d/data/MediaSet.java b/new3d/src/com/android/gallery3d/data/MediaSet.java
index 319ffe4..176edeb 100644
--- a/new3d/src/com/android/gallery3d/data/MediaSet.java
+++ b/new3d/src/com/android/gallery3d/data/MediaSet.java
@@ -16,52 +16,68 @@
 
 package com.android.gallery3d.data;
 
-// This is currently implemented MediaSet and MediaItem:
-//
-//           | Local | Picasa
-// ----------+----------------
-//  AlbumSet |   1   |    2
-//  Album    |   3   |    4
-//  Image    |   5   |    6
-//  Video    |   7   | (unimplemented)
-//
-//  Inheritance relation:
-//
-//  MediaSet -- DatabaseMediaSet -- {1,2,3,4}
-//  MediaItem -- LocalMediaItem -- {5, 7}
-//            -- {6}
-//
-//  root = ComboMediaSet (LocalAlbumSet, PicasaAlbumSet);
-
+import java.util.ArrayList;
 
 // MediaSet is a directory-like data structure.
 // It contains MediaItems and sub-MediaSets.
 //
 // getTotalMediaItemCount() returns the number of all MediaItems, including
 // those in sub-MediaSets.
-//
-// getCoverMediaItems() return a few representative MediaItems for this
-// MediaSet.
-//
 public abstract class MediaSet {
 
     public interface MediaSetListener {
         public void onContentChanged();
     }
 
-    public abstract int getMediaItemCount();
+    public int getMediaItemCount() {
+        return 0;
+    }
 
-    public abstract MediaItem getMediaItem(int index);
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        throw new IndexOutOfBoundsException();
+    }
 
-    public abstract int getSubMediaSetCount();
+    // This is for compatibility only.
+    public MediaItem getMediaItem(int index) {
+        ArrayList<MediaItem> items = getMediaItem(index, 1);
+        if (items.size() > 0) {
+            return items.get(0);
+        } else {
+            return null;
+        }
+    }
 
-    public abstract MediaSet getSubMediaSet(int index);
+    // This is for compatibility only.
+    public MediaItem[] getCoverMediaItems() {
+        if (getMediaItemCount() > 0) {
+            ArrayList<MediaItem> items = getMediaItem(0, 4);
+            MediaItem result[] = new MediaItem[items.size()];
+            return items.toArray(result);
+        } else if (getSubMediaSetCount() > 0) {
+            return getSubMediaSet(0).getCoverMediaItems();
+        } else {
+            return new MediaItem[0];
+        }
+    }
+
+    public int getSubMediaSetCount() {
+        return 0;
+    }
+
+    public MediaSet getSubMediaSet(int index) {
+        throw new IndexOutOfBoundsException();
+    }
 
     public abstract int getTotalMediaItemCount();
 
-    public abstract String getTitle();
+    public abstract long getId();
+    public abstract String getName();
 
-    public abstract MediaItem[] getCoverMediaItems();
+    protected MediaSetListener mListener;
 
-    public abstract void setContentListener(MediaSetListener listener);
+    public void setContentListener(MediaSetListener listener) {
+        mListener = listener;
+    }
+
+    public abstract void reload();
 }
diff --git a/new3d/src/com/android/gallery3d/data/MergeAlbum.java b/new3d/src/com/android/gallery3d/data/MergeAlbum.java
new file mode 100644
index 0000000..75e2a8a
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/data/MergeAlbum.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.util.Log;
+
+import java.util.Comparator;
+import java.util.TreeMap;
+import java.util.ArrayList;
+import java.util.SortedMap;
+
+// Merge two MediaSets.
+// This only handles MediaItems, not SubMediaSets.
+public class MergeAlbum extends MediaSet implements MediaSet.MediaSetListener {
+    private static final String TAG = "MergeAlbum";
+    private final long mUniqueId;
+    private final int mPageSize;
+    private final Comparator<MediaItem> mComparator;
+    private final MediaSet[] mSets;
+    private final int mSize;  // caches mSets.length
+    private FetchCache[] mFetcher;
+
+    // mIndex maps global position to the position of each underlying media sets.
+    private TreeMap<Integer, int[]> mIndex;
+
+    public MergeAlbum(long uniqueId, int pageSize, Comparator<MediaItem> comparator,
+            MediaSet[] mediaSets) {
+        mUniqueId = uniqueId;
+        mPageSize = pageSize;
+        mComparator = comparator;
+        mSets = mediaSets;
+        mSize = mSets.length;
+        mFetcher = new FetchCache[mSize];
+        for (int i = 0; i < mSize; i++) {
+            MediaSet s = mSets[i];
+            s.setContentListener(this);
+            mFetcher[i] = new FetchCache(s, mPageSize);
+        }
+        mIndex = new TreeMap<Integer, int[]>();
+        mIndex.put(0, new int[mSize]);
+    }
+
+    private void invalidateCache() {
+        for (int i = 0; i < mSize; i++) {
+            mFetcher[i].invalidate();
+        }
+        mIndex.clear();
+        mIndex.put(0, new int[mSize]);
+    }
+
+    public long getId() {
+        return mUniqueId;
+    }
+
+    public String getName() {
+        return TAG;
+    }
+
+    public int getMediaItemCount() {
+        return getTotalMediaItemCount();
+    }
+
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+
+        // First find the nearest mark position <= start.
+        SortedMap<Integer, int[]> head = mIndex.headMap(start + 1);
+        int markPos = head.lastKey();
+        int[] subPos = (int []) head.get(markPos).clone();
+        MediaItem[] slot = new MediaItem[mSize];
+
+        // fill all slots
+        for (int i = 0; i < mSize; i++) {
+            slot[i] = mFetcher[i].getItem(subPos[i]);
+        }
+
+        ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+
+        for (int i = markPos; i < start + count; i++) {
+            int k = -1;  // k points to the best slot up to now.
+            for (int j = 0; j < mSize; j++) {
+                if (slot[j] != null) {
+                    if (k == -1 || mComparator.compare(slot[j], slot[k]) < 0) {
+                        k = j;
+                    }
+                }
+            }
+
+            // If we don't have anything, all streams are exhausted.
+            if (k == -1) break;
+
+            // Pick the best slot and refill it.
+            subPos[k]++;
+            if (i >= start) {
+                result.add(slot[k]);
+                Log.v(TAG, "added " + ((LocalMediaItem)slot[k]).mDateTakenInMs + " for " + i);
+            }
+            slot[k] = mFetcher[k].getItem(subPos[k]);
+
+            // Periodically leave a mark in the index, so we can come back later.
+            if ((i + 1) % mPageSize == 0) {
+                mIndex.put(i + 1, (int[]) subPos.clone());
+            }
+        }
+
+        return result;
+    }
+
+    public int getTotalMediaItemCount() {
+        int count = 0;
+        for (MediaSet set : mSets) {
+            count += set.getTotalMediaItemCount();
+        }
+        return count;
+    }
+
+    public void reload() {
+        for (MediaSet set : mSets) {
+            set.reload();
+        }
+    }
+
+    public void onContentChanged() {
+        invalidateCache();
+        if (mListener != null) {
+            mListener.onContentChanged();
+        }
+    }
+}
+
+class FetchCache {
+    private static final String TAG = "FetchCache";
+    private MediaSet mBaseSet;
+    private int mPageSize;
+    private ArrayList<MediaItem> mCache;
+    private int mStartPos;
+
+    FetchCache(MediaSet baseSet, int pageSize) {
+        mBaseSet = baseSet;
+        mPageSize = pageSize;
+    }
+
+    void invalidate() {
+        mCache = null;
+    }
+
+    MediaItem getItem(int index) {
+        boolean needLoading = false;
+        if (mCache == null) {
+            needLoading = true;
+        } else if (index < mStartPos || index >= mStartPos + mPageSize) {
+            needLoading = true;
+        }
+
+        if (needLoading) {
+            mCache = mBaseSet.getMediaItem(index, mPageSize);
+            mStartPos = index;
+        }
+
+        if (index < mStartPos || index >= mStartPos + mCache.size()) {
+            return null;
+        }
+
+        return mCache.get(index - mStartPos);
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/data/MergeAlbumSet.java b/new3d/src/com/android/gallery3d/data/MergeAlbumSet.java
new file mode 100644
index 0000000..a53eab9
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/data/MergeAlbumSet.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.data;
+
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.TreeMap;
+
+// Merge two media sets into one.
+// If the two media sets have sub media sets with the same name,
+// they will be merged.
+public class MergeAlbumSet extends MediaSet implements MediaSet.MediaSetListener {
+    private static final String TAG = "MergeAlbumSet";
+    private static final int PAGE_SIZE = 100;
+    private final long mUniqueId;
+    private Comparator<MediaItem> mComparator;
+    private final MediaSet[] mSets;
+    private MediaSet[] mAlbums;
+
+    public MergeAlbumSet(long uniqueId, Comparator<MediaItem> comparator,
+            MediaSet ... mediaSets) {
+        mUniqueId = uniqueId;
+        mComparator = comparator;
+        mSets = mediaSets;
+        updateNames();
+        for (MediaSet set : mediaSets) {
+            set.setContentListener(this);
+        }
+    }
+
+    public long getId() {
+        return mUniqueId;
+    }
+
+    private void updateNames() {
+
+        // This map maps from a the item id to a list of media sets.
+        // The list of media sets are the media set with that item id.
+        TreeMap<Integer, ArrayList<MediaSet>> map =
+                new TreeMap<Integer, ArrayList<MediaSet>>();
+        for (MediaSet set : mSets) {
+            for (int i = 0, n = set.getSubMediaSetCount(); i < n; i++) {
+                MediaSet subset = set.getSubMediaSet(i);
+                int itemId = DataManager.extractItemId(subset.getId());
+                ArrayList<MediaSet> list = map.get(itemId);
+                if (list == null) {
+                    list = new ArrayList<MediaSet>();
+                    map.put(itemId, list);
+                }
+                list.add(subset);
+            }
+        }
+
+        int size = map.size();
+        mAlbums = new MediaSet[size];
+
+        int i = 0;
+        for (Map.Entry<Integer, ArrayList<MediaSet>> entry : map.entrySet()) {
+            ArrayList<MediaSet> list = entry.getValue();
+            if (list.size() == 1) {
+                mAlbums[i] = list.get(0);
+            } else {
+                int itemId = entry.getKey();
+                long id = DataManager.makeId(DataManager.ID_MERGE_LOCAL_ALBUM,
+                        itemId);
+                MediaSet[] sets = new MediaSet[list.size()];
+                mAlbums[i] = new MergeAlbum(id, PAGE_SIZE, mComparator,
+                        list.toArray(sets));
+            }
+            i = i + 1;
+        }
+    }
+
+    public MediaSet getSubMediaSet(int index) {
+        return mAlbums[index];
+    }
+
+    public int getSubMediaSetCount() {
+        return mAlbums.length;
+    }
+
+    public String getName() {
+        return TAG;
+    }
+
+    public int getTotalMediaItemCount() {
+        int count = 0;
+        for (MediaSet set : mSets) {
+            count += set.getTotalMediaItemCount();
+        }
+        return count;
+    }
+
+    public void reload() {
+        for (MediaSet set : mSets) {
+            set.reload();
+        }
+    }
+
+    public void onContentChanged() {
+        updateNames();
+        if (mListener != null) {
+            mListener.onContentChanged();
+        }
+    }
+}
diff --git a/new3d/src/com/android/gallery3d/data/PicasaAlbum.java b/new3d/src/com/android/gallery3d/data/PicasaAlbum.java
index 28a6ada..8784334 100644
--- a/new3d/src/com/android/gallery3d/data/PicasaAlbum.java
+++ b/new3d/src/com/android/gallery3d/data/PicasaAlbum.java
@@ -16,85 +16,94 @@
 
 package com.android.gallery3d.data;
 
+import android.content.ContentResolver;
 import android.database.Cursor;
+import android.net.Uri;
 
 import com.android.gallery3d.app.GalleryContext;
 import com.android.gallery3d.picasa.AlbumEntry;
 import com.android.gallery3d.picasa.EntrySchema;
 import com.android.gallery3d.picasa.PhotoEntry;
 import com.android.gallery3d.picasa.PicasaContentProvider;
+import com.android.gallery3d.util.Utils;
 
 import java.util.ArrayList;
 
-public class PicasaAlbum extends DatabaseMediaSet {
-    private static final int MAX_COVER_COUNT = 4;
+public class PicasaAlbum extends MediaSet {
+    private static final String TAG = "PicasaAlbum";
     private static final EntrySchema SCHEMA = PhotoEntry.SCHEMA;
-    private static final String WHERECLAUSE = PhotoEntry.Columns.ALBUM_ID
+    private static final String WHERE_CLAUSE = PhotoEntry.Columns.ALBUM_ID
             + " = ?";
+    private static final String[] COUNT_PROJECTION = { "count(*)" };
 
     private final AlbumEntry mData;
-    private final ArrayList<PicasaImage> mPhotos = new ArrayList<PicasaImage>();
-    private final ArrayList<PicasaImage> mLoadBuffer = new ArrayList<PicasaImage>();
+    private final ContentResolver mResolver;
+    private long mUniqueId;
+    private GalleryContext mContext;
 
     public PicasaAlbum(GalleryContext context, AlbumEntry entry) {
-        super(context);
+        mContext = context;
+        mResolver = context.getContentResolver();
         mData = entry;
+        mUniqueId = DataManager.makeId(
+                DataManager.ID_PICASA_ALBUM, (int) entry.id);
     }
 
-    public MediaItem[] getCoverMediaItems() {
-        int size = Math.min(MAX_COVER_COUNT, mPhotos.size());
-        MediaItem items[] = new MediaItem[size];
-        for (int i = 0; i < size; ++i) {
-            items[i] = mPhotos.get(i);
-        }
-        return items;
+    public long getId() {
+        return mUniqueId;
     }
 
-    public MediaItem getMediaItem(int index) {
-        return mPhotos.get(index);
-    }
+    public ArrayList<MediaItem> getMediaItem(int start, int count) {
+        Uri uri = PicasaContentProvider.PHOTOS_URI.buildUpon()
+                .appendQueryParameter("limit", start + "," + count).build();
 
-    public int getMediaItemCount() {
-        return mPhotos.size();
-    }
-
-    public MediaSet getSubMediaSet(int index) {
-        throw new IndexOutOfBoundsException();
-    }
-
-    public int getSubMediaSetCount() {
-        return 0;
-    }
-
-    public String getTitle() {
-        return null;
-    }
-
-    public int getTotalMediaItemCount() {
-        return mPhotos.size();
-    }
-
-    @Override
-    protected void onLoadFromDatabase() {
-        Cursor cursor = mContext.getContentResolver().query(
-                PicasaContentProvider.PHOTOS_URI,
-                SCHEMA.getProjection(), WHERECLAUSE,
-                new String[] {String.valueOf(mData.id)},
+        ArrayList<MediaItem> list = new ArrayList<MediaItem>();
+        Cursor cursor = mResolver.query(uri,
+                SCHEMA.getProjection(), WHERE_CLAUSE,
+                new String[]{String.valueOf(mData.id)},
                 PhotoEntry.Columns.DISPLAY_INDEX);
+
         try {
             while (cursor.moveToNext()) {
                 PhotoEntry entry = SCHEMA.cursorToObject(cursor, new PhotoEntry());
-                mLoadBuffer.add(new PicasaImage(mContext, entry));
+                DataManager dataManager = mContext.getDataManager();
+                long uniqueId = DataManager.makeId(
+                        DataManager.ID_PICASA_IMAGE, (int) entry.id);
+                MediaItem item = dataManager.getFromCache(uniqueId);
+                if (item == null) {
+                    item = new PicasaImage(mContext, entry);
+                    dataManager.putToCache(uniqueId, item);
+                }
+                list.add(item);
             }
         } finally {
             cursor.close();
         }
+        return list;
+    }
+
+    public int getMediaItemCount() {
+        Cursor cursor = mResolver.query(
+                PicasaContentProvider.PHOTOS_URI,
+                COUNT_PROJECTION, WHERE_CLAUSE,
+                new String[]{String.valueOf(mData.id)}, null);
+        try {
+            Utils.Assert(cursor.moveToNext());
+            return cursor.getInt(0);
+        } finally {
+            cursor.close();
+        }
     }
 
-    @Override
-    protected void onUpdateContent() {
-        mPhotos.clear();
-        mPhotos.addAll(mLoadBuffer);
-        mLoadBuffer.clear();
+    public String getName() {
+        return TAG;
+    }
+
+    public int getTotalMediaItemCount() {
+        return getMediaItemCount();
+    }
+
+    public void reload() {
+        // do nothing
     }
 }
diff --git a/new3d/src/com/android/gallery3d/data/PicasaAlbumSet.java b/new3d/src/com/android/gallery3d/data/PicasaAlbumSet.java
index a56378d..dc8c65c 100644
--- a/new3d/src/com/android/gallery3d/data/PicasaAlbumSet.java
+++ b/new3d/src/com/android/gallery3d/data/PicasaAlbumSet.java
@@ -26,28 +26,16 @@
 import java.util.ArrayList;
 
 public class PicasaAlbumSet extends DatabaseMediaSet {
+    private static final String TAG = "PicasaAlbumSet";
     private final EntrySchema SCHEMA = AlbumEntry.SCHEMA;
 
     private final ArrayList<PicasaAlbum> mAlbums = new ArrayList<PicasaAlbum>();
-    private int mCachedTotalCount = -1;
     private final ArrayList<PicasaAlbum> mLoadBuffer = new ArrayList<PicasaAlbum>();
 
     public PicasaAlbumSet(GalleryContext context) {
         super(context);
     }
 
-    public MediaItem[] getCoverMediaItems() {
-        throw new UnsupportedOperationException();
-    }
-
-    public MediaItem getMediaItem(int index) {
-        throw new IndexOutOfBoundsException();
-    }
-
-    public int getMediaItemCount() {
-        return 0;
-    }
-
     public MediaSet getSubMediaSet(int index) {
         return mAlbums.get(index);
     }
@@ -56,24 +44,26 @@
         return mAlbums.size();
     }
 
-    public String getTitle() {
-        return null;
+    public String getName() {
+        return TAG;
+    }
+
+    public long getId() {
+        return DataManager.makeId(DataManager.ID_PICASA_ALBUM_SET, 0);
     }
 
     public int getTotalMediaItemCount() {
-        if (mCachedTotalCount >= 0) return mCachedTotalCount;
         int totalCount = 0;
         for (PicasaAlbum album : mAlbums) {
             totalCount += album.getTotalMediaItemCount();
         }
-        mCachedTotalCount = totalCount;
         return totalCount;
     }
 
     @Override
     protected void onLoadFromDatabase() {
         mLoadBuffer.clear();
-        Cursor cursor = mContext.getContentResolver().query(
+        Cursor cursor = mResolver.query(
                 PicasaContentProvider.ALBUMS_URI,
                 SCHEMA.getProjection(), null, null, null);
         try {
@@ -91,9 +81,5 @@
         mAlbums.clear();
         mAlbums.addAll(mLoadBuffer);
         mLoadBuffer.clear();
-
-        for (PicasaAlbum album : mAlbums) {
-            album.invalidate();
-        }
     }
 }
diff --git a/new3d/src/com/android/gallery3d/data/PicasaImage.java b/new3d/src/com/android/gallery3d/data/PicasaImage.java
index 666d48b..f465a0e 100644
--- a/new3d/src/com/android/gallery3d/data/PicasaImage.java
+++ b/new3d/src/com/android/gallery3d/data/PicasaImage.java
@@ -40,15 +40,18 @@
     private final GalleryContext mContext;
     private final PhotoEntry mData;
     private final BlobCache mPicasaCache;
+    private final long mUniqueId;
 
     public PicasaImage(GalleryContext context, PhotoEntry entry) {
         mContext = context;
         mData = entry;
         mPicasaCache = mContext.getDataManager().getPicasaCache();
+        mUniqueId = DataManager.makeId(
+                DataManager.ID_PICASA_IMAGE, (int) entry.id);
     }
 
-    public String getTitle() {
-        return null;
+    public long getUniqueId() {
+        return mUniqueId;
     }
 
     public synchronized Future<Bitmap>
diff --git a/new3d/src/com/android/gallery3d/util/IdentityCache.java b/new3d/src/com/android/gallery3d/util/IdentityCache.java
new file mode 100644
index 0000000..321ca38
--- /dev/null
+++ b/new3d/src/com/android/gallery3d/util/IdentityCache.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.util;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+
+public class IdentityCache<K, V> {
+
+    private final HashMap<K, Entry<K, V>> mWeakMap =
+            new HashMap<K, Entry<K, V>>();
+    private ReferenceQueue<V> mQueue = new ReferenceQueue<V>();
+
+    public IdentityCache() {
+    }
+
+    private static class Entry<K, V> extends WeakReference<V> {
+        K mKey;
+
+        public Entry(K key, V value, ReferenceQueue<V> queue) {
+            super(value, queue);
+            mKey = key;
+        }
+    }
+
+    private void cleanUpWeakMap() {
+        Entry<K, V> entry = (Entry<K, V>) mQueue.poll();
+        while (entry != null) {
+            mWeakMap.remove(entry.mKey);
+            entry = (Entry<K, V>) mQueue.poll();
+        }
+    }
+
+    public synchronized V put(K key, V value) {
+        cleanUpWeakMap();
+        Entry<K, V> entry = mWeakMap.put(
+                key, new Entry<K, V>(key, value, mQueue));
+        return entry == null ? null : entry.get();
+    }
+
+    public synchronized V get(K key) {
+        cleanUpWeakMap();
+        Entry<K, V> entry = mWeakMap.get(key);
+        return entry == null ? null : entry.get();
+    }
+
+    public synchronized void clear() {
+        mWeakMap.clear();
+        mQueue = new ReferenceQueue<V>();
+    }
+}
diff --git a/new3d/tests/src/com/android/gallery3d/ui/GLRootMock.java b/new3d/tests/src/com/android/gallery3d/ui/GLRootMock.java
index db07e48..c83e943 100644
--- a/new3d/tests/src/com/android/gallery3d/ui/GLRootMock.java
+++ b/new3d/tests/src/com/android/gallery3d/ui/GLRootMock.java
@@ -33,4 +33,5 @@
     public boolean hasStencil() { return true; }
     public void lockRenderThread() {}
     public void unlockRenderThread() {}
+    public void setContentPane(GLView content) {}
 }
diff --git a/new3d/tests/src/com/android/gallery3d/ui/GLRootStub.java b/new3d/tests/src/com/android/gallery3d/ui/GLRootStub.java
index 955cc18..d6bc678 100644
--- a/new3d/tests/src/com/android/gallery3d/ui/GLRootStub.java
+++ b/new3d/tests/src/com/android/gallery3d/ui/GLRootStub.java
@@ -26,4 +26,5 @@
     public boolean hasStencil() { return true; }
     public void lockRenderThread() {}
     public void unlockRenderThread() {}
+    public void setContentPane(GLView content) {}
 }