OSDN Git Service

android-2.1_r1 snapshot
[android-x86/packages-apps-Gallery2.git] / src / com / cooliris / media / MediaFeed.java
diff --git a/src/com/cooliris/media/MediaFeed.java b/src/com/cooliris/media/MediaFeed.java
new file mode 100644 (file)
index 0000000..62fa084
--- /dev/null
@@ -0,0 +1,743 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.widget.Toast;
+import android.os.Process;
+
+import com.cooliris.media.MediaClustering.Cluster;
+
+public final class MediaFeed implements Runnable {
+    public static final int OPERATION_DELETE = 0;
+    public static final int OPERATION_ROTATE = 1;
+    public static final int OPERATION_CROP = 2;
+
+    private static final int NUM_ITEMS_LOOKAHEAD = 60;
+
+    private IndexRange mVisibleRange = new IndexRange();
+    private IndexRange mBufferedRange = new IndexRange();
+    private ArrayList<MediaSet> mMediaSets = new ArrayList<MediaSet>();
+    private Listener mListener;
+    private DataSource mDataSource;
+    private boolean mListenerNeedsUpdate = false;
+    private boolean mMediaFeedNeedsToRun = false;
+    private MediaSet mSingleWrapper = new MediaSet();
+    private boolean mInClusteringMode = false;
+    private HashMap<MediaSet, MediaClustering> mClusterSets = new HashMap<MediaSet, MediaClustering>(32);
+    private int mExpandedMediaSetIndex = Shared.INVALID;
+    private MediaFilter mMediaFilter;
+    private MediaSet mMediaFilteredSet;
+    private Context mContext;
+    private Thread mDataSourceThread = null;
+    private Thread mAlbumSourceThread = null;
+    private boolean mListenerNeedsLayout;
+    private boolean mWaitingForMediaScanner;
+    private boolean mSingleImageMode;
+    private boolean mLoading;
+
+    public interface Listener {
+        public abstract void onFeedAboutToChange(MediaFeed feed);
+
+        public abstract void onFeedChanged(MediaFeed feed, boolean needsLayout);
+    }
+
+    public MediaFeed(Context context, DataSource dataSource, Listener listener) {
+        mContext = context;
+        mListener = listener;
+        mDataSource = dataSource;
+        mSingleWrapper.setNumExpectedItems(1);
+        mLoading = true;
+    }
+
+    public void shutdown() {
+        if (mDataSourceThread != null) {
+            mDataSource.shutdown();
+            mDataSourceThread.interrupt();
+            mDataSourceThread = null;
+        }
+        if (mAlbumSourceThread != null) {
+            mAlbumSourceThread.interrupt();
+            mAlbumSourceThread = null;
+        }
+        int numSets = mMediaSets.size();
+        for (int i = 0; i < numSets; ++i) {
+            MediaSet set = mMediaSets.get(i);
+            set.clear();
+        }
+        synchronized (mMediaSets) {
+            mMediaSets.clear();
+        }
+        int numClusters = mClusterSets.size();
+        for (int i = 0; i < numClusters; ++i) {
+            MediaClustering mc = mClusterSets.get(i);
+            if (mc != null) {
+                mc.clear();
+            }
+        }
+        mClusterSets.clear();
+        mListener = null;
+        mDataSource = null;
+        mSingleWrapper = null;
+    }
+
+    public void setVisibleRange(int begin, int end) {
+        if (begin != mVisibleRange.begin || end != mVisibleRange.end) {
+            mVisibleRange.begin = begin;
+            mVisibleRange.end = end;
+            int numItems = 96;
+            int numItemsBy2 = numItems / 2;
+            int numItemsBy4 = numItems / 4;
+            mBufferedRange.begin = (begin / numItemsBy2) * numItemsBy2 - numItemsBy4;
+            mBufferedRange.end = mBufferedRange.begin + numItems;
+            mMediaFeedNeedsToRun = true;
+        }
+    }
+
+    public void setFilter(MediaFilter filter) {
+        mMediaFilter = filter;
+        mMediaFilteredSet = null;
+        if (mListener != null) {
+            mListener.onFeedAboutToChange(this);
+        }
+        mMediaFeedNeedsToRun = true;
+    }
+
+    public void removeFilter() {
+        mMediaFilter = null;
+        mMediaFilteredSet = null;
+        if (mListener != null) {
+            mListener.onFeedAboutToChange(this);
+            updateListener(true);
+        }
+        mMediaFeedNeedsToRun = true;
+    }
+
+    public ArrayList<MediaSet> getMediaSets() {
+        return mMediaSets;
+    }
+
+    public MediaSet getMediaSet(final long setId) {
+        if (setId != Shared.INVALID) {
+            try {
+                int mMediaSetsSize = mMediaSets.size();
+                for (int i = 0; i < mMediaSetsSize; i++) {
+                    if (mMediaSets.get(i).mId == setId) {
+                        return mMediaSets.get(i);
+                    }
+                }
+            } catch (Exception e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    public MediaSet getFilteredSet() {
+        return mMediaFilteredSet;
+    }
+
+    public MediaSet addMediaSet(final long setId, DataSource dataSource) {
+        MediaSet mediaSet = new MediaSet(dataSource);
+        mediaSet.mId = setId;
+        mMediaSets.add(mediaSet);
+        if (mDataSourceThread != null && !mDataSourceThread.isAlive()) {
+            mDataSourceThread.start();
+        }
+        mMediaFeedNeedsToRun = true;
+        return mediaSet;
+    }
+
+    public DataSource getDataSource() {
+        return mDataSource;
+    }
+
+    public MediaClustering getClustering() {
+        if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
+            return mClusterSets.get(mMediaSets.get(mExpandedMediaSetIndex));
+        }
+        return null;
+    }
+
+    public ArrayList<Cluster> getClustersForSet(final MediaSet set) {
+        ArrayList<Cluster> clusters = null;
+        if (mClusterSets != null && mClusterSets.containsKey(set)) {
+            MediaClustering mediaClustering = mClusterSets.get(set);
+            if (mediaClustering != null) {
+                clusters = mediaClustering.getClusters();
+            }
+        }
+        return clusters;
+    }
+
+    public void addItemToMediaSet(MediaItem item, MediaSet mediaSet) {
+        item.mParentMediaSet = mediaSet;
+        mediaSet.addItem(item);
+        synchronized (mClusterSets) {
+            if (item.mClusteringState == MediaItem.NOT_CLUSTERED) {
+                MediaClustering clustering = mClusterSets.get(mediaSet);
+                if (clustering == null) {
+                    clustering = new MediaClustering(mediaSet.isPicassaAlbum());
+                    mClusterSets.put(mediaSet, clustering);
+                }
+                clustering.setTimeRange(mediaSet.mMaxTimestamp - mediaSet.mMinTimestamp, mediaSet.getNumExpectedItems());
+                clustering.addItemForClustering(item);
+                item.mClusteringState = MediaItem.CLUSTERED;
+            }
+        }
+        mMediaFeedNeedsToRun = true;
+    }
+
+    public void performOperation(final int operation, final ArrayList<MediaBucket> mediaBuckets, final Object data) {
+        int numBuckets = mediaBuckets.size();
+        final ArrayList<MediaBucket> copyMediaBuckets = new ArrayList<MediaBucket>(numBuckets);
+        for (int i = 0; i < numBuckets; ++i) {
+            copyMediaBuckets.add(mediaBuckets.get(i));
+        }
+        if (operation == OPERATION_DELETE && mListener != null) {
+            mListener.onFeedAboutToChange(this);
+        }
+        Thread operationThread = new Thread(new Runnable() {
+            public void run() {
+                ArrayList<MediaBucket> mediaBuckets = copyMediaBuckets;
+                if (operation == OPERATION_DELETE) {
+                    int numBuckets = mediaBuckets.size();
+                    for (int i = 0; i < numBuckets; ++i) {
+                        MediaBucket bucket = mediaBuckets.get(i);
+                        MediaSet set = bucket.mediaSet;
+                        ArrayList<MediaItem> items = bucket.mediaItems;
+                        if (set != null && items == null) {
+                            // Remove the entire bucket.
+                            removeMediaSet(set);
+                        } else if (set != null && items != null) {
+                            // We need to remove these items from the set.
+                            int numItems = items.size();
+                            // We also need to delete the items from the
+                            // cluster.
+                            MediaClustering clustering = mClusterSets.get(set);
+                            for (int j = 0; j < numItems; ++j) {
+                                MediaItem item = items.get(j);
+                                removeItemFromMediaSet(item, set);
+                                if (clustering != null) {
+                                    clustering.removeItemFromClustering(item);
+                                }
+                            }
+                            set.updateNumExpectedItems();
+                            set.generateTitle(true);
+                        }
+                    }
+                    updateListener(true);
+                    mMediaFeedNeedsToRun = true;
+                    if (mDataSource != null) {
+                        mDataSource.performOperation(OPERATION_DELETE, mediaBuckets, null);
+                    }
+                } else {
+                    mDataSource.performOperation(operation, mediaBuckets, data);
+                }
+            }
+        });
+        operationThread.setName("Operation " + operation);
+        operationThread.start();
+    }
+
+    public void removeMediaSet(MediaSet set) {
+        synchronized (mMediaSets) {
+            mMediaSets.remove(set);
+        }
+        mMediaFeedNeedsToRun = true;
+    }
+
+    private void removeItemFromMediaSet(MediaItem item, MediaSet mediaSet) {
+        mediaSet.removeItem(item);
+        synchronized (mClusterSets) {
+            MediaClustering clustering = mClusterSets.get(mediaSet);
+            if (clustering != null) {
+                clustering.removeItemFromClustering(item);
+            }
+        }
+        mMediaFeedNeedsToRun = true;
+    }
+
+    public void updateListener(boolean needsLayout) {
+        mListenerNeedsUpdate = true;
+        mListenerNeedsLayout = needsLayout;
+    }
+
+    public int getNumSlots() {
+        int currentMediaSetIndex = mExpandedMediaSetIndex;
+        ArrayList<MediaSet> mediaSets = mMediaSets;
+        int mediaSetsSize = mediaSets.size();
+
+        if (mInClusteringMode == false) {
+            if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize) {
+                return mediaSetsSize;
+            } else {
+                MediaSet setToUse = (mMediaFilteredSet == null) ? mediaSets.get(currentMediaSetIndex) : mMediaFilteredSet;
+                return setToUse.getNumItems();
+            }
+        } else if (currentMediaSetIndex != Shared.INVALID && currentMediaSetIndex < mediaSetsSize) {
+            MediaSet set = mediaSets.get(currentMediaSetIndex);
+            MediaClustering clustering = mClusterSets.get(set);
+            if (clustering != null) {
+                return clustering.getClustersForDisplay().size();
+            }
+        }
+        return 0;
+    }
+
+    public MediaSet getSetForSlot(int slotIndex) {
+        if (slotIndex < 0) {
+            return null;
+        }
+
+        ArrayList<MediaSet> mediaSets = mMediaSets;
+        int mediaSetsSize = mediaSets.size();
+        int currentMediaSetIndex = mExpandedMediaSetIndex;
+
+        if (mInClusteringMode == false) {
+            if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize) {
+                if (slotIndex >= mediaSetsSize) {
+                    return null;
+                }
+                return mMediaSets.get(slotIndex);
+            }
+            if (mSingleWrapper.getNumItems() == 0) {
+                mSingleWrapper.addItem(null);
+            }
+            MediaSet setToUse = (mMediaFilteredSet == null) ? mMediaSets.get(currentMediaSetIndex) : mMediaFilteredSet;
+            ArrayList<MediaItem> items = setToUse.getItems();
+            if (slotIndex >= setToUse.getNumItems()) {
+                return null;
+            }
+            mSingleWrapper.getItems().set(0, items.get(slotIndex));
+            return mSingleWrapper;
+        } else if (currentMediaSetIndex != Shared.INVALID && currentMediaSetIndex < mediaSetsSize) {
+            MediaSet set = mediaSets.get(currentMediaSetIndex);
+            MediaClustering clustering = mClusterSets.get(set);
+            if (clustering != null) {
+                ArrayList<MediaClustering.Cluster> clusters = clustering.getClustersForDisplay();
+                if (clusters.size() > slotIndex) {
+                    MediaClustering.Cluster cluster = clusters.get(slotIndex);
+                    cluster.generateCaption(mContext);
+                    return cluster;
+                }
+            }
+        }
+        return null;
+    }
+
+    public boolean getWaitingForMediaScanner() {
+        return mWaitingForMediaScanner;
+    }
+
+    public boolean isLoading() {
+        return mLoading;
+    }
+
+    public void start() {
+        final MediaFeed feed = this;
+        mLoading = true;
+        mDataSourceThread = new Thread(this);
+        mDataSourceThread.setName("MediaFeed");
+        mAlbumSourceThread = new Thread(new Runnable() {
+            public void run() {
+                if (mContext == null)
+                    return;
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                DataSource dataSource = mDataSource;
+                // We must wait while the SD card is mounted or the MediaScanner
+                // is running.
+                if (dataSource != null) {
+                    dataSource.loadMediaSets(feed);
+                }
+                mWaitingForMediaScanner = false;
+                while (ImageManager.isMediaScannerScanning(mContext.getContentResolver())) {
+                    // MediaScanner is still running, wait
+                    if (Thread.interrupted())
+                        return;
+                    mWaitingForMediaScanner = true;
+                    try {
+                        if (mContext == null)
+                            return;
+                        showToast(mContext.getResources().getString(R.string.initializing), Toast.LENGTH_LONG);
+                        Thread.sleep(6000);
+                    } catch (InterruptedException e) {
+                        return;
+                    }
+                }
+                if (mWaitingForMediaScanner) {
+                    showToast(mContext.getResources().getString(R.string.loading_new), Toast.LENGTH_LONG);
+                    mWaitingForMediaScanner = false;
+                    if (dataSource != null) {
+                        dataSource.loadMediaSets(feed);
+                    }
+                }
+                mLoading = false;
+            }
+        });
+        mAlbumSourceThread.setName("MediaSets");
+        mAlbumSourceThread.start();
+    }
+
+    private void showToast(final String string, final int duration) {
+        showToast(string, duration, false);
+    }
+
+    private void showToast(final String string, final int duration, final boolean centered) {
+        if (mContext != null && !((Gallery) mContext).isPaused()) {
+            ((Gallery) mContext).getHandler().post(new Runnable() {
+                public void run() {
+                    if (mContext != null) {
+                        Toast toast = Toast.makeText(mContext, string, duration);
+                        if (centered) {
+                            toast.setGravity(Gravity.CENTER, 0, 0);
+                        }
+                        toast.show();
+                    }
+                }
+            });
+        }
+    }
+
+    public void run() {
+        DataSource dataSource = mDataSource;
+        int sleepMs = 10;
+        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+        if (dataSource != null) {
+            while (!Thread.interrupted()) {
+                if (mListenerNeedsUpdate) {
+                    mListenerNeedsUpdate = false;
+                    if (mListener != null)
+                        mListener.onFeedChanged(this, mListenerNeedsLayout);
+                    try {
+                        Thread.sleep(sleepMs);
+                    } catch (InterruptedException e) {
+                        return;
+                    }
+                } else {
+                    if (mWaitingForMediaScanner) {
+                        synchronized (mMediaSets) {
+                            mMediaSets.clear();
+                        }
+                    }
+                    try {
+                        Thread.sleep(sleepMs);
+                    } catch (InterruptedException e) {
+                        return;
+                    }
+                }
+                sleepMs = 300;
+                if (!mMediaFeedNeedsToRun)
+                    continue;
+                if (((Gallery) mContext).isPaused())
+                    continue;
+                mMediaFeedNeedsToRun = false;
+                ArrayList<MediaSet> mediaSets = mMediaSets;
+                synchronized (mediaSets) {
+                    int expandedSetIndex = mExpandedMediaSetIndex;
+                    if (expandedSetIndex >= mMediaSets.size()) {
+                        expandedSetIndex = Shared.INVALID;
+                    }
+                    if (expandedSetIndex == Shared.INVALID) {
+                        // We purge the sets outside this visibleRange.
+                        int numSets = mediaSets.size();
+                        IndexRange visibleRange = mVisibleRange;
+                        IndexRange bufferedRange = mBufferedRange;
+                        boolean scanMediaSets = true;
+                        for (int i = 0; i < numSets; ++i) {
+                            if (i >= visibleRange.begin && i <= visibleRange.end && scanMediaSets) {
+                                MediaSet set = mediaSets.get(i);
+                                int numItemsLoaded = set.mNumItemsLoaded;
+                                if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
+                                    dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
+                                    if (set.getNumExpectedItems() == 0) {
+                                        mediaSets.remove(set);
+                                        break;
+                                    }
+                                    if (mListener != null) {
+                                        mListener.onFeedChanged(this, false);
+                                    }
+                                    sleepMs = 100;
+                                    scanMediaSets = false;
+                                }
+                                if (!set.setContainsValidItems()) {
+                                    mediaSets.remove(set);
+                                    if (mListener != null) {
+                                        mListener.onFeedChanged(this, false);
+                                    }
+                                    break;
+                                }
+                            }
+                        }
+                        numSets = mediaSets.size();
+                        for (int i = 0; i < numSets; ++i) {
+                            MediaSet set = mediaSets.get(i);
+                            if (i >= bufferedRange.begin && i <= bufferedRange.end) {
+                                if (scanMediaSets) {
+                                    int numItemsLoaded = set.mNumItemsLoaded;
+                                    if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
+                                        dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
+                                        if (set.getNumExpectedItems() == 0) {
+                                            mediaSets.remove(set);
+                                            break;
+                                        }
+                                        if (mListener != null) {
+                                            mListener.onFeedChanged(this, false);
+                                        }
+                                        sleepMs = 100;
+                                        scanMediaSets = false;
+                                    }
+                                }
+                            } else if (i < bufferedRange.begin || i > bufferedRange.end) {
+                                // Purge this set to its initial status.
+                                MediaClustering clustering = mClusterSets.get(set);
+                                if (clustering != null) {
+                                    clustering.clear();
+                                    mClusterSets.remove(set);
+                                }
+                                if (set.getNumItems() != 0)
+                                    set.clear();
+                            }
+                        }
+                    }
+                    if (expandedSetIndex != Shared.INVALID) {
+                        int numSets = mMediaSets.size();
+                        for (int i = 0; i < numSets; ++i) {
+                            // Purge other sets.
+                            if (i != expandedSetIndex) {
+                                MediaSet set = mediaSets.get(i);
+                                MediaClustering clustering = mClusterSets.get(set);
+                                if (clustering != null) {
+                                    clustering.clear();
+                                    mClusterSets.remove(set);
+                                }
+                                if (set.getNumItems() != 0)
+                                    set.clear();
+                            }
+                        }
+                        // Make sure all the items are loaded for the album.
+                        int numItemsLoaded = mediaSets.get(expandedSetIndex).mNumItemsLoaded;
+                        int requestedItems = mVisibleRange.end;
+                        // requestedItems count changes in clustering mode.
+                        if (mInClusteringMode && mClusterSets != null) {
+                            requestedItems = 0;
+                            MediaClustering clustering = mClusterSets.get(mediaSets.get(expandedSetIndex));
+                            if (clustering != null) {
+                                ArrayList<Cluster> clusters = clustering.getClustersForDisplay();
+                                int numClusters = clusters.size();
+                                for (int i = 0; i < numClusters; i++) {
+                                    requestedItems += clusters.get(i).getNumExpectedItems();
+                                }
+                            }
+                        }
+                        MediaSet set = mediaSets.get(expandedSetIndex);
+                        if (numItemsLoaded < set.getNumExpectedItems()) {
+                            // We perform calculations for a window that gets anchored to a multiple of NUM_ITEMS_LOOKAHEAD.
+                            // The start of the window is 0, x, 2x, 3x ... etc where x = NUM_ITEMS_LOOKAHEAD.
+                            dataSource.loadItemsForSet(this, set, numItemsLoaded, (requestedItems / NUM_ITEMS_LOOKAHEAD)
+                                    * NUM_ITEMS_LOOKAHEAD + NUM_ITEMS_LOOKAHEAD);
+                            if (set.getNumExpectedItems() == 0) {
+                                mediaSets.remove(set);
+                                mListener.onFeedChanged(this, false);
+                            }
+                            if (numItemsLoaded != set.mNumItemsLoaded && mListener != null) {
+                                mListener.onFeedChanged(this, false);
+                            }
+                        }
+                    }
+                    MediaFilter filter = mMediaFilter;
+                    if (filter != null && mMediaFilteredSet == null) {
+                        if (expandedSetIndex != Shared.INVALID) {
+                            MediaSet set = mediaSets.get(expandedSetIndex);
+                            ArrayList<MediaItem> items = set.getItems();
+                            int numItems = set.getNumItems();
+                            MediaSet filteredSet = new MediaSet();
+                            filteredSet.setNumExpectedItems(numItems);
+                            mMediaFilteredSet = filteredSet;
+                            for (int i = 0; i < numItems; ++i) {
+                                MediaItem item = items.get(i);
+                                if (filter.pass(item)) {
+                                    filteredSet.addItem(item);
+                                }
+                            }
+                            filteredSet.updateNumExpectedItems();
+                            filteredSet.generateTitle(true);
+                        }
+                        updateListener(true);
+                    }
+                }
+            }
+        }
+    }
+
+    public void expandMediaSet(int mediaSetIndex) {
+        // We need to check if this slot can be focused or not.
+        if (mListener != null) {
+            mListener.onFeedAboutToChange(this);
+        }
+        if (mExpandedMediaSetIndex > 0 && mediaSetIndex == Shared.INVALID) {
+            // We are collapsing a previously expanded media set
+            if (mediaSetIndex < mMediaSets.size() && mExpandedMediaSetIndex >= 0 && mExpandedMediaSetIndex < mMediaSets.size()) {
+                MediaSet set = mMediaSets.get(mExpandedMediaSetIndex);
+                if (set.getNumItems() == 0) {
+                    set.clear();
+                }
+            }
+        }
+        mExpandedMediaSetIndex = mediaSetIndex;
+        if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
+            // Notify Picasa that the user entered the album.
+            // MediaSet set = mMediaSets.get(mediaSetIndex);
+            // PicasaService.requestSync(mContext,
+            // PicasaService.TYPE_ALBUM_PHOTOS, set.mPicasaAlbumId);
+        }
+        updateListener(true);
+        mMediaFeedNeedsToRun = true;
+    }
+
+    public boolean canExpandSet(int slotIndex) {
+        int mediaSetIndex = slotIndex;
+        if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
+            MediaSet set = mMediaSets.get(mediaSetIndex);
+            if (set.getNumItems() > 0) {
+                MediaItem item = set.getItems().get(0);
+                if (item.mId == Shared.INVALID) {
+                    return false;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean hasExpandedMediaSet() {
+        return (mExpandedMediaSetIndex != Shared.INVALID);
+    }
+
+    public boolean restorePreviousClusteringState() {
+        boolean retVal = disableClusteringIfNecessary();
+        if (retVal) {
+            if (mListener != null) {
+                mListener.onFeedAboutToChange(this);
+            }
+            updateListener(true);
+            mMediaFeedNeedsToRun = true;
+        }
+        return retVal;
+    }
+
+    private boolean disableClusteringIfNecessary() {
+        if (mInClusteringMode) {
+            // Disable clustering.
+            mInClusteringMode = false;
+            mMediaFeedNeedsToRun = true;
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isClustered() {
+        return mInClusteringMode;
+    }
+
+    public MediaSet getCurrentSet() {
+        if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
+            return mMediaSets.get(mExpandedMediaSetIndex);
+        }
+        return null;
+    }
+
+    public void performClustering() {
+        if (mListener != null) {
+            mListener.onFeedAboutToChange(this);
+        }
+        MediaSet setToUse = null;
+        if (mExpandedMediaSetIndex != Shared.INVALID || mExpandedMediaSetIndex < mMediaSets.size()) {
+            setToUse = mMediaSets.get(mExpandedMediaSetIndex);
+        }
+        if (setToUse != null) {
+            MediaClustering clustering = null;
+            synchronized (mClusterSets) {
+                // Make sure the computation is completed to the end.
+                clustering = mClusterSets.get(setToUse);
+                if (clustering != null) {
+                    clustering.compute(null, true);
+                } else {
+                    return;
+                }
+            }
+            mInClusteringMode = true;
+            mMediaFeedNeedsToRun = true;
+            updateListener(true);
+        }
+    }
+
+    public void moveSetToFront(MediaSet mediaSet) {
+        ArrayList<MediaSet> mediaSets = mMediaSets;
+        int numSets = mediaSets.size();
+        if (numSets == 0) {
+            mediaSets.add(mediaSet);
+            return;
+        }
+        MediaSet setToFind = mediaSets.get(0);
+        if (setToFind == mediaSet) {
+            return;
+        }
+        mediaSets.set(0, mediaSet);
+        int indexToSwapTill = -1;
+        for (int i = 1; i < numSets; ++i) {
+            MediaSet set = mediaSets.get(i);
+            if (set == mediaSet) {
+                mediaSets.set(i, setToFind);
+                indexToSwapTill = i;
+                break;
+            }
+        }
+        if (indexToSwapTill != Shared.INVALID) {
+            for (int i = indexToSwapTill; i > 1; --i) {
+                MediaSet setEnd = mediaSets.get(i);
+                MediaSet setPrev = mediaSets.get(i - 1);
+                mediaSets.set(i, setPrev);
+                mediaSets.set(i - 1, setEnd);
+            }
+        }
+        mMediaFeedNeedsToRun = true;
+    }
+
+    public MediaSet replaceMediaSet(long setId, DataSource dataSource) {
+        MediaSet mediaSet = new MediaSet(dataSource);
+        mediaSet.mId = setId;
+        ArrayList<MediaSet> mediaSets = mMediaSets;
+        int numSets = mediaSets.size();
+        for (int i = 0; i < numSets; ++i) {
+            final MediaSet thisSet = mediaSets.get(i);
+            if (thisSet.mId == setId) {
+                mediaSet.mName = thisSet.mName;
+                mediaSet.mHasImages = thisSet.mHasImages;
+                mediaSet.mHasVideos = thisSet.mHasVideos;
+                mediaSets.set(i, mediaSet);
+                break;
+            }
+        }
+        mMediaFeedNeedsToRun = true;
+        return mediaSet;
+    }
+
+    public void setSingleImageMode(boolean singleImageMode) {
+        mSingleImageMode = singleImageMode;
+    }
+
+    public boolean isSingleImageMode() {
+        return mSingleImageMode;
+    }
+
+    public MediaSet getExpandedMediaSet() {
+        if (mExpandedMediaSetIndex == Shared.INVALID)
+            return null;
+        if (mExpandedMediaSetIndex >= mMediaSets.size())
+            return null;
+        return mMediaSets.get(mExpandedMediaSetIndex);
+    }
+}