public final class

MediaFeed

extends Object
implements Runnable
/*
 * Copyright (C) 2009 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.cooliris.media;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.util.Log;
import android.view.Gravity;
import android.widget.Toast;
import android.net.Uri;
import android.os.Handler;
import android.os.Process;

import com.cooliris.app.App;
import com.cooliris.app.Res;
import com.cooliris.media.MediaClustering.Cluster;

public final class MediaFeed implements Runnable {
    private final String TAG = "MediaFeed";
    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 static final int NUM_INTERRUPT_RETRIES = 30;
    private static final int JOIN_TIMEOUT = 50;

    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;
    private HashMap<String, ContentObserver> mContentObservers = new HashMap<String, ContentObserver>();
    private ArrayList<String[]> mRequestedRefresh = new ArrayList<String[]>();
    private volatile boolean mIsShutdown = false;

    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() {
        mIsShutdown = true;
        if (mDataSourceThread != null) {
            mDataSource.shutdown();
            repeatShuttingDownThread(mDataSourceThread);
            mDataSourceThread = null;
        }
        if (mAlbumSourceThread != null) {
            repeatShuttingDownThread(mAlbumSourceThread);
            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;
    }

    private void repeatShuttingDownThread(Thread targetThread) {
        for (int i = 0; i < NUM_INTERRUPT_RETRIES && targetThread.isAlive(); ++i) {
            targetThread.interrupt();
            try {
                targetThread.join(JOIN_TIMEOUT);
            } catch (InterruptedException e) {
                Log.w(TAG, "Cannot stop the thread: " + targetThread.getName(), e);
                Thread.currentThread().interrupt();
                return;
            }
        }

        if (targetThread.isAlive()) {
            Log.w(TAG, "Cannot stop the thread: " + targetThread.getName());
        }
    }

    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++) {
                    final MediaSet set = mMediaSets.get(i);
                    if (set.mId == setId) {
                        set.mFlagForDelete = false;
                        return set;
                    }
                }
            } catch (Exception e) {
                return null;
            }
        }
        return null;
    }

    public MediaSet getFilteredSet() {
        return mMediaFilteredSet;
    }

    public MediaSet addMediaSet(final long setId, DataSource dataSource) {
        int numSets = mMediaSets.size();
        for (int i = 0; i < numSets; i++) {
            MediaSet set = mMediaSets.get(i);
            if ((set.mId == setId) && (set.mDataSource == dataSource)) {
                // The mediaset already exists, but might be out-dated.
                // To avoid the same mediaset being added twice, we delete
                // the old one first, then add the new one below.
                mMediaSets.remove(i);
                break;
            }
        }

        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.getNumExpectedItems();
            }
        } 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 void copySlotStateFrom(MediaFeed another) {
        mExpandedMediaSetIndex = another.mExpandedMediaSetIndex;
        mInClusteringMode = another.mInClusteringMode;
    }

    public ArrayList<Integer> getBreaks() {
        if (true)
            return null;
        int currentMediaSetIndex = mExpandedMediaSetIndex;
        ArrayList<MediaSet> mediaSets = mMediaSets;
        int mediaSetsSize = mediaSets.size();
        if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize)
            return null;
        MediaSet set = mediaSets.get(currentMediaSetIndex);
        MediaClustering clustering = mClusterSets.get(set);
        if (clustering != null) {
            clustering.compute(null, true);
            final ArrayList<Cluster> clusters = clustering.getClustersForDisplay();
            int numClusters = clusters.size();
            final ArrayList<Integer> retVal = new ArrayList<Integer>(numClusters);
            int size = 0;
            for (int i = 0; i < numClusters; ++i) {
                size += clusters.get(i).getItems().size();
                retVal.add(size);
            }
            return retVal;
        } else {
            return null;
        }
    }

    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;
        onResume();
        mLoading = true;
        mDataSourceThread = new Thread(this);
        mDataSourceThread.setName("MediaFeed");
        mIsShutdown = false;
        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) {
                    loadMediaSets();
                }
                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(Res.string.initializing), Toast.LENGTH_LONG);
                        if (dataSource != null) {
                            loadMediaSets();
                        }
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        return;
                    }
                }
                if (mWaitingForMediaScanner) {
                    showToast(mContext.getResources().getString(Res.string.loading_new), Toast.LENGTH_LONG);
                    mWaitingForMediaScanner = false;
                    loadMediaSets();
                }
                mLoading = false;
            }
        });
        mAlbumSourceThread.setName("MediaSets");
        mAlbumSourceThread.start();
    }

    private void loadMediaSets() {
        if (mDataSource == null)
            return;
        final ArrayList<MediaSet> sets = mMediaSets;
        synchronized (sets) {
            final int numSets = sets.size();
            for (int i = 0; i < numSets; ++i) {
                final MediaSet set = sets.get(i);
                set.mFlagForDelete = true;
            }
            mDataSource.refresh(MediaFeed.this, mDataSource.getDatabaseUris());
            mDataSource.loadMediaSets(MediaFeed.this);
            final ArrayList<MediaSet> setsToRemove = new ArrayList<MediaSet>();
            for (int i = 0; i < numSets; ++i) {
                final MediaSet set = sets.get(i);
                if (set.mFlagForDelete) {
                    setsToRemove.add(set);
                }
            }
            int numSetsToRemove = setsToRemove.size();
            for (int i = 0; i < numSetsToRemove; ++i) {
                sets.remove(setsToRemove.get(i));
            }
            setsToRemove.clear();
        }
        mMediaFeedNeedsToRun = true;
        updateListener(false);
    }

    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 && !App.get(mContext).isPaused()) {
            App.get(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() && !mIsShutdown) {
                String[] databaseUris = null;
                boolean performRefresh = false;
                synchronized (mRequestedRefresh) {
                    if (mRequestedRefresh.size() > 0) {
                        // We prune this first.
                        int numRequests = mRequestedRefresh.size();
                        for (int i = 0; i < numRequests; ++i) {
                            databaseUris = ArrayUtils.addAll(databaseUris, mRequestedRefresh.get(i));
                        }
                        mRequestedRefresh.clear();
                        performRefresh = true;
                        // We need to eliminate duplicate uris in this array
                        final HashMap<String, String> uris = new HashMap<String, String>();
                        if (databaseUris != null) {
                            int numUris = databaseUris.length;
                            for (int i = 0; i < numUris; ++i) {
                                final String uri = databaseUris[i];
                                if (uri != null)
                                    uris.put(uri, uri);
                            }
                        }
                        databaseUris = new String[0];
                        databaseUris = (String[]) uris.keySet().toArray(databaseUris);
                    }
                }
                boolean settingFeedAboutToChange = false;
                if (performRefresh) {
                    if (dataSource != null) {
                        if (mListener != null) {
                            settingFeedAboutToChange = true;
                            mListener.onFeedAboutToChange(this);
                        }
                        dataSource.refresh(this, databaseUris);
                        mMediaFeedNeedsToRun = true;
                    }
                }
                if (mListenerNeedsUpdate && !mMediaFeedNeedsToRun) {
                    mListenerNeedsUpdate = false;
                    if (mListener != null)
                        synchronized (mMediaSets) {
                            mListener.onFeedChanged(this, mListenerNeedsLayout);
                        }
                    try {
                        Thread.sleep(sleepMs);
                    } catch (InterruptedException e) {
                        return;
                    }
                } else {
                    try {
                        Thread.sleep(sleepMs);
                    } catch (InterruptedException e) {
                        return;
                    }
                }
                sleepMs = 300;
                if (!mMediaFeedNeedsToRun)
                    continue;
                App app = App.get(mContext);
                if (app == null || app.isPaused())
                    continue;
                if (settingFeedAboutToChange) {
                    updateListener(true);
                }
                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) {
                                    synchronized (set) {
                                        dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
                                        set.checkForDeletedItems();
                                    }
                                    if (set.getNumExpectedItems() == 0) {
                                        mediaSets.remove(set);
                                        break;
                                    }
                                    if (mListener != null) {
                                        mListenerNeedsUpdate = false;
                                        mListener.onFeedChanged(this, mListenerNeedsLayout);
                                        mListenerNeedsLayout = false;
                                    }
                                    sleepMs = 100;
                                    scanMediaSets = false;
                                }
                                if (!set.setContainsValidItems()) {
                                    mediaSets.remove(set);
                                    if (mListener != null) {
                                        mListenerNeedsUpdate = false;
                                        mListener.onFeedChanged(this, mListenerNeedsLayout);
                                        mListenerNeedsLayout = 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) {
                                        synchronized (set) {
                                            dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
                                            set.checkForDeletedItems();
                                        }
                                        if (set.getNumExpectedItems() == 0) {
                                            mediaSets.remove(set);
                                            break;
                                        }
                                        if (mListener != null) {
                                            mListenerNeedsUpdate = false;
                                            mListener.onFeedChanged(this, mListenerNeedsLayout);
                                            mListenerNeedsLayout = false;
                                        }
                                        sleepMs = 100;
                                        scanMediaSets = false;
                                    }
                                }
                            } else if (!mListenerNeedsUpdate && (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.mNumItemsLoaded != 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.
                            synchronized (set) {
                                dataSource.loadItemsForSet(this, set, numItemsLoaded, (requestedItems / NUM_ITEMS_LOOKAHEAD)
                                        * NUM_ITEMS_LOOKAHEAD + NUM_ITEMS_LOOKAHEAD);
                                set.checkForDeletedItems();
                            }
                            if (set.getNumExpectedItems() == 0) {
                                mediaSets.remove(set);
                                mListenerNeedsUpdate = false;
                                mListener.onFeedChanged(this, mListenerNeedsLayout);
                                mListenerNeedsLayout = false;
                            }
                            if (numItemsLoaded != set.mNumItemsLoaded && mListener != null) {
                                mListenerNeedsUpdate = false;
                                mListener.onFeedChanged(this, mListenerNeedsLayout);
                                mListenerNeedsLayout = 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;
            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) {
        Log.i(TAG, "Replacing media set " + setId);
        final MediaSet set = getMediaSet(setId);
        if (set != null)
            set.refresh();
        return set;
    }

    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);
    }

    public void refresh() {
        if (mDataSource != null) {
            synchronized (mRequestedRefresh) {
                mRequestedRefresh.add(mDataSource.getDatabaseUris());
            }
        }
    }

    private void refresh(final String[] databaseUris) {
        synchronized (mMediaSets) {
            if (mDataSource != null) {
                synchronized (mRequestedRefresh) {
                    mRequestedRefresh.add(databaseUris);
                }
            }
        }
    }

    public void onPause() {
        final HashMap<String, ContentObserver> observers = mContentObservers;
        final int numObservers = observers.size();
        if (numObservers > 0) {
            String[] uris = new String[numObservers];
            final Set<String> keySet = observers.keySet();
            if (keySet != null) {
                uris = keySet.toArray(uris);
                final int numUris = uris.length;
                final ContentResolver cr = mContext.getContentResolver();
                for (int i = 0; i < numUris; ++i) {
                    final String uri = uris[i];
                    if (uri != null) {
                        final ContentObserver observer = observers.get(uri);
                        cr.unregisterContentObserver(observer);
                        observers.remove(uri);
                    }
                }
            }
        }
        observers.clear();
    }

    public void onResume() {
        final Context context = mContext;
        final DataSource dataSource = mDataSource;
        if (context == null || dataSource == null)
            return;
        // We setup the listeners for this datasource
        final String[] uris = dataSource.getDatabaseUris();
        final HashMap<String, ContentObserver> observers = mContentObservers;
        if (context instanceof Gallery) {
            final Gallery gallery = (Gallery) context;
            final ContentResolver cr = context.getContentResolver();
            if (uris != null) {
                final int numUris = uris.length;
                for (int i = 0; i < numUris; ++i) {
                    final String uri = uris[i];
                    final ContentObserver presentObserver = observers.get(uri);
                    if (presentObserver == null) {
                        final Handler handler = App.get(context).getHandler();
                        final ContentObserver observer = new ContentObserver(handler) {
                            public void onChange(boolean selfChange) {
                                if (!mWaitingForMediaScanner) {
                                    MediaFeed.this.refresh(new String[] { uri });
                                }
                            }
                        };
                        cr.registerContentObserver(Uri.parse(uri), true, observer);
                        observers.put(uri, observer);
                    }
                }
            }
        }
        refresh();
    }
}