1 package com.cooliris.media;
3 import java.util.ArrayList;
4 import java.util.HashMap;
6 import android.content.Context;
7 import android.view.Gravity;
8 import android.widget.Toast;
9 import android.os.Process;
11 import com.cooliris.media.MediaClustering.Cluster;
13 public final class MediaFeed implements Runnable {
14 public static final int OPERATION_DELETE = 0;
15 public static final int OPERATION_ROTATE = 1;
16 public static final int OPERATION_CROP = 2;
18 private static final int NUM_ITEMS_LOOKAHEAD = 60;
20 private IndexRange mVisibleRange = new IndexRange();
21 private IndexRange mBufferedRange = new IndexRange();
22 private ArrayList<MediaSet> mMediaSets = new ArrayList<MediaSet>();
23 private Listener mListener;
24 private DataSource mDataSource;
25 private boolean mListenerNeedsUpdate = false;
26 private boolean mMediaFeedNeedsToRun = false;
27 private MediaSet mSingleWrapper = new MediaSet();
28 private boolean mInClusteringMode = false;
29 private HashMap<MediaSet, MediaClustering> mClusterSets = new HashMap<MediaSet, MediaClustering>(32);
30 private int mExpandedMediaSetIndex = Shared.INVALID;
31 private MediaFilter mMediaFilter;
32 private MediaSet mMediaFilteredSet;
33 private Context mContext;
34 private Thread mDataSourceThread = null;
35 private Thread mAlbumSourceThread = null;
36 private boolean mListenerNeedsLayout;
37 private boolean mWaitingForMediaScanner;
38 private boolean mSingleImageMode;
39 private boolean mLoading;
41 public interface Listener {
42 public abstract void onFeedAboutToChange(MediaFeed feed);
44 public abstract void onFeedChanged(MediaFeed feed, boolean needsLayout);
47 public MediaFeed(Context context, DataSource dataSource, Listener listener) {
50 mDataSource = dataSource;
51 mSingleWrapper.setNumExpectedItems(1);
55 public void shutdown() {
56 if (mDataSourceThread != null) {
57 mDataSource.shutdown();
58 mDataSourceThread.interrupt();
59 mDataSourceThread = null;
61 if (mAlbumSourceThread != null) {
62 mAlbumSourceThread.interrupt();
63 mAlbumSourceThread = null;
65 int numSets = mMediaSets.size();
66 for (int i = 0; i < numSets; ++i) {
67 MediaSet set = mMediaSets.get(i);
70 synchronized (mMediaSets) {
73 int numClusters = mClusterSets.size();
74 for (int i = 0; i < numClusters; ++i) {
75 MediaClustering mc = mClusterSets.get(i);
83 mSingleWrapper = null;
86 public void setVisibleRange(int begin, int end) {
87 if (begin != mVisibleRange.begin || end != mVisibleRange.end) {
88 mVisibleRange.begin = begin;
89 mVisibleRange.end = end;
91 int numItemsBy2 = numItems / 2;
92 int numItemsBy4 = numItems / 4;
93 mBufferedRange.begin = (begin / numItemsBy2) * numItemsBy2 - numItemsBy4;
94 mBufferedRange.end = mBufferedRange.begin + numItems;
95 mMediaFeedNeedsToRun = true;
99 public void setFilter(MediaFilter filter) {
100 mMediaFilter = filter;
101 mMediaFilteredSet = null;
102 if (mListener != null) {
103 mListener.onFeedAboutToChange(this);
105 mMediaFeedNeedsToRun = true;
108 public void removeFilter() {
110 mMediaFilteredSet = null;
111 if (mListener != null) {
112 mListener.onFeedAboutToChange(this);
113 updateListener(true);
115 mMediaFeedNeedsToRun = true;
118 public ArrayList<MediaSet> getMediaSets() {
122 public MediaSet getMediaSet(final long setId) {
123 if (setId != Shared.INVALID) {
125 int mMediaSetsSize = mMediaSets.size();
126 for (int i = 0; i < mMediaSetsSize; i++) {
127 if (mMediaSets.get(i).mId == setId) {
128 return mMediaSets.get(i);
131 } catch (Exception e) {
138 public MediaSet getFilteredSet() {
139 return mMediaFilteredSet;
142 public MediaSet addMediaSet(final long setId, DataSource dataSource) {
143 MediaSet mediaSet = new MediaSet(dataSource);
144 mediaSet.mId = setId;
145 mMediaSets.add(mediaSet);
146 if (mDataSourceThread != null && !mDataSourceThread.isAlive()) {
147 mDataSourceThread.start();
149 mMediaFeedNeedsToRun = true;
153 public DataSource getDataSource() {
157 public MediaClustering getClustering() {
158 if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
159 return mClusterSets.get(mMediaSets.get(mExpandedMediaSetIndex));
164 public ArrayList<Cluster> getClustersForSet(final MediaSet set) {
165 ArrayList<Cluster> clusters = null;
166 if (mClusterSets != null && mClusterSets.containsKey(set)) {
167 MediaClustering mediaClustering = mClusterSets.get(set);
168 if (mediaClustering != null) {
169 clusters = mediaClustering.getClusters();
175 public void addItemToMediaSet(MediaItem item, MediaSet mediaSet) {
176 item.mParentMediaSet = mediaSet;
177 mediaSet.addItem(item);
178 synchronized (mClusterSets) {
179 if (item.mClusteringState == MediaItem.NOT_CLUSTERED) {
180 MediaClustering clustering = mClusterSets.get(mediaSet);
181 if (clustering == null) {
182 clustering = new MediaClustering(mediaSet.isPicassaAlbum());
183 mClusterSets.put(mediaSet, clustering);
185 clustering.setTimeRange(mediaSet.mMaxTimestamp - mediaSet.mMinTimestamp, mediaSet.getNumExpectedItems());
186 clustering.addItemForClustering(item);
187 item.mClusteringState = MediaItem.CLUSTERED;
190 mMediaFeedNeedsToRun = true;
193 public void performOperation(final int operation, final ArrayList<MediaBucket> mediaBuckets, final Object data) {
194 int numBuckets = mediaBuckets.size();
195 final ArrayList<MediaBucket> copyMediaBuckets = new ArrayList<MediaBucket>(numBuckets);
196 for (int i = 0; i < numBuckets; ++i) {
197 copyMediaBuckets.add(mediaBuckets.get(i));
199 if (operation == OPERATION_DELETE && mListener != null) {
200 mListener.onFeedAboutToChange(this);
202 Thread operationThread = new Thread(new Runnable() {
204 ArrayList<MediaBucket> mediaBuckets = copyMediaBuckets;
205 if (operation == OPERATION_DELETE) {
206 int numBuckets = mediaBuckets.size();
207 for (int i = 0; i < numBuckets; ++i) {
208 MediaBucket bucket = mediaBuckets.get(i);
209 MediaSet set = bucket.mediaSet;
210 ArrayList<MediaItem> items = bucket.mediaItems;
211 if (set != null && items == null) {
212 // Remove the entire bucket.
214 } else if (set != null && items != null) {
215 // We need to remove these items from the set.
216 int numItems = items.size();
217 // We also need to delete the items from the cluster.
218 MediaClustering clustering = mClusterSets.get(set);
219 for (int j = 0; j < numItems; ++j) {
220 MediaItem item = items.get(j);
221 removeItemFromMediaSet(item, set);
222 if (clustering != null) {
223 clustering.removeItemFromClustering(item);
226 set.updateNumExpectedItems();
227 set.generateTitle(true);
230 updateListener(true);
231 mMediaFeedNeedsToRun = true;
232 if (mDataSource != null) {
233 mDataSource.performOperation(OPERATION_DELETE, mediaBuckets, null);
236 mDataSource.performOperation(operation, mediaBuckets, data);
240 operationThread.setName("Operation " + operation);
241 operationThread.start();
244 public void removeMediaSet(MediaSet set) {
245 mMediaSets.remove(set);
246 mMediaFeedNeedsToRun = true;
249 private void removeItemFromMediaSet(MediaItem item, MediaSet mediaSet) {
250 mediaSet.removeItem(item);
251 synchronized (mClusterSets) {
252 MediaClustering clustering = mClusterSets.get(mediaSet);
253 if (clustering != null) {
254 clustering.removeItemFromClustering(item);
257 mMediaFeedNeedsToRun = true;
260 public void updateListener(boolean needsLayout) {
261 mListenerNeedsUpdate = true;
262 mListenerNeedsLayout = needsLayout;
265 public int getNumSlots() {
266 int currentMediaSetIndex = mExpandedMediaSetIndex;
267 ArrayList<MediaSet> mediaSets = mMediaSets;
268 int mediaSetsSize = mediaSets.size();
270 if (mInClusteringMode == false) {
271 if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize) {
272 return mediaSetsSize;
274 MediaSet setToUse = (mMediaFilteredSet == null) ? mediaSets.get(currentMediaSetIndex) : mMediaFilteredSet;
275 return setToUse.getNumItems();
277 } else if (currentMediaSetIndex != Shared.INVALID && currentMediaSetIndex < mediaSetsSize) {
278 MediaSet set = mediaSets.get(currentMediaSetIndex);
279 MediaClustering clustering = mClusterSets.get(set);
280 if (clustering != null) {
281 return clustering.getClustersForDisplay().size();
287 public MediaSet getSetForSlot(int slotIndex) {
292 ArrayList<MediaSet> mediaSets = mMediaSets;
293 int mediaSetsSize = mediaSets.size();
294 int currentMediaSetIndex = mExpandedMediaSetIndex;
296 if (mInClusteringMode == false) {
297 if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize) {
298 if (slotIndex >= mediaSetsSize) {
301 return mMediaSets.get(slotIndex);
303 if (mSingleWrapper.getNumItems() == 0) {
304 mSingleWrapper.addItem(null);
306 MediaSet setToUse = (mMediaFilteredSet == null) ? mMediaSets.get(currentMediaSetIndex) : mMediaFilteredSet;
307 ArrayList<MediaItem> items = setToUse.getItems();
308 if (slotIndex >= setToUse.getNumItems()) {
311 mSingleWrapper.getItems().set(0, items.get(slotIndex));
312 return mSingleWrapper;
313 } else if (currentMediaSetIndex != Shared.INVALID && currentMediaSetIndex < mediaSetsSize) {
314 MediaSet set = mediaSets.get(currentMediaSetIndex);
315 MediaClustering clustering = mClusterSets.get(set);
316 if (clustering != null) {
317 ArrayList<MediaClustering.Cluster> clusters = clustering.getClustersForDisplay();
318 if (clusters.size() > slotIndex) {
319 MediaClustering.Cluster cluster = clusters.get(slotIndex);
320 cluster.generateCaption(mContext);
328 public boolean getWaitingForMediaScanner() {
329 return mWaitingForMediaScanner;
332 public boolean isLoading() {
336 public void start() {
337 final MediaFeed feed = this;
339 mDataSourceThread = new Thread(this);
340 mDataSourceThread.setName("MediaFeed");
341 mAlbumSourceThread = new Thread(new Runnable() {
343 if (mContext == null)
345 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
346 DataSource dataSource = mDataSource;
347 // We must wait while the SD card is mounted or the MediaScanner is running.
348 if (dataSource != null) {
349 dataSource.loadMediaSets(feed);
351 mWaitingForMediaScanner = false;
352 while (ImageManager.isMediaScannerScanning(mContext.getContentResolver())) {
353 // MediaScanner is still running, wait
354 mWaitingForMediaScanner = true;
356 if (mContext == null)
358 showToast(mContext.getResources().getString(R.string.initializing), Toast.LENGTH_LONG);
360 } catch (InterruptedException e) {
364 if (mWaitingForMediaScanner) {
365 showToast(mContext.getResources().getString(R.string.loading_new), Toast.LENGTH_LONG);
366 mWaitingForMediaScanner = false;
367 if (dataSource != null) {
368 dataSource.loadMediaSets(feed);
374 mAlbumSourceThread.setName("MediaSets");
375 mAlbumSourceThread.start();
378 private void showToast(final String string, final int duration) {
379 showToast(string, duration, false);
382 private void showToast(final String string, final int duration, final boolean centered) {
383 if (mContext != null && !((Gallery) mContext).isPaused()) {
384 ((Gallery) mContext).getHandler().post(new Runnable() {
386 if (mContext != null) {
387 Toast toast = Toast.makeText(mContext, string, duration);
389 toast.setGravity(Gravity.CENTER, 0, 0);
399 DataSource dataSource = mDataSource;
401 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
402 if (dataSource != null) {
403 while (!Thread.interrupted()) {
404 if (mListenerNeedsUpdate) {
405 mListenerNeedsUpdate = false;
406 if (mListener != null)
407 mListener.onFeedChanged(this, mListenerNeedsLayout);
409 Thread.sleep(sleepMs);
410 } catch (InterruptedException e) {
414 if (mWaitingForMediaScanner) {
415 synchronized (mMediaSets) {
420 Thread.sleep(sleepMs);
421 } catch (InterruptedException e) {
426 if (!mMediaFeedNeedsToRun)
428 mMediaFeedNeedsToRun = false;
429 ArrayList<MediaSet> mediaSets = mMediaSets;
430 synchronized (mediaSets) {
431 int expandedSetIndex = mExpandedMediaSetIndex;
432 if (expandedSetIndex >= mMediaSets.size()) {
433 expandedSetIndex = Shared.INVALID;
435 if (expandedSetIndex == Shared.INVALID) {
436 // We purge the sets outside this visibleRange.
437 int numSets = mMediaSets.size();
438 IndexRange visibleRange = mVisibleRange;
439 IndexRange bufferedRange = mBufferedRange;
440 boolean scanMediaSets = true;
441 for (int i = 0; i < numSets; ++i) {
442 if (i >= visibleRange.begin && i <= visibleRange.end && scanMediaSets) {
443 MediaSet set = mediaSets.get(i);
444 int numItemsLoaded = set.mNumItemsLoaded;
445 if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
446 dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
447 if (set.getNumExpectedItems() == 0) {
448 mediaSets.remove(set);
451 if (mListener != null) {
452 mListener.onFeedChanged(this, false);
455 scanMediaSets = false;
457 if (!set.setContainsValidItems()) {
458 mediaSets.remove(set);
459 if (mListener != null) {
460 mListener.onFeedChanged(this, false);
466 numSets = mMediaSets.size();
467 for (int i = 0; i < numSets; ++i) {
468 MediaSet set = mediaSets.get(i);
469 if (i >= bufferedRange.begin && i <= bufferedRange.end) {
471 int numItemsLoaded = set.mNumItemsLoaded;
472 if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
473 dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
474 if (set.getNumExpectedItems() == 0) {
475 mediaSets.remove(set);
478 if (mListener != null) {
479 mListener.onFeedChanged(this, false);
482 scanMediaSets = false;
485 } else if (i < bufferedRange.begin || i > bufferedRange.end) {
486 // Purge this set to its initial status.
487 MediaClustering clustering = mClusterSets.get(set);
488 if (clustering != null) {
490 mClusterSets.remove(set);
492 if (set.getNumItems() != 0)
497 if (expandedSetIndex != Shared.INVALID) {
498 int numSets = mMediaSets.size();
499 for (int i = 0; i < numSets; ++i) {
501 if (i != expandedSetIndex) {
502 MediaSet set = mediaSets.get(i);
503 MediaClustering clustering = mClusterSets.get(set);
504 if (clustering != null) {
506 mClusterSets.remove(set);
508 if (set.getNumItems() != 0)
512 // Make sure all the items are loaded for the album.
513 int numItemsLoaded = mediaSets.get(expandedSetIndex).mNumItemsLoaded;
514 int requestedItems = mVisibleRange.end;
515 // requestedItems count changes in clustering mode.
516 if (mInClusteringMode && mClusterSets != null) {
518 MediaClustering clustering = mClusterSets.get(mediaSets.get(expandedSetIndex));
519 ArrayList<Cluster> clusters = clustering.getClustersForDisplay();
520 int numClusters = clusters.size();
521 for (int i = 0; i < numClusters; i++) {
522 requestedItems += clusters.get(i).getNumExpectedItems();
525 MediaSet set = mediaSets.get(expandedSetIndex);
526 if (numItemsLoaded < set.getNumExpectedItems()) {
527 // TODO(Venkat) Why are we doing 4th param calculations like this?
528 dataSource.loadItemsForSet(this, set, numItemsLoaded, (requestedItems / NUM_ITEMS_LOOKAHEAD)
529 * NUM_ITEMS_LOOKAHEAD + NUM_ITEMS_LOOKAHEAD);
530 if (set.getNumExpectedItems() == 0) {
531 mediaSets.remove(set);
532 mListener.onFeedChanged(this, false);
534 if (numItemsLoaded != set.mNumItemsLoaded && mListener != null) {
535 mListener.onFeedChanged(this, false);
539 MediaFilter filter = mMediaFilter;
540 if (filter != null && mMediaFilteredSet == null) {
541 if (expandedSetIndex != Shared.INVALID) {
542 MediaSet set = mediaSets.get(expandedSetIndex);
543 ArrayList<MediaItem> items = set.getItems();
544 int numItems = set.getNumItems();
545 MediaSet filteredSet = new MediaSet();
546 filteredSet.setNumExpectedItems(numItems);
547 mMediaFilteredSet = filteredSet;
548 for (int i = 0; i < numItems; ++i) {
549 MediaItem item = items.get(i);
550 if (filter.pass(item)) {
551 filteredSet.addItem(item);
554 filteredSet.updateNumExpectedItems();
555 filteredSet.generateTitle(true);
557 updateListener(true);
564 public void expandMediaSet(int mediaSetIndex) {
565 // We need to check if this slot can be focused or not.
566 if (mListener != null) {
567 mListener.onFeedAboutToChange(this);
569 if (mExpandedMediaSetIndex > 0 && mediaSetIndex == Shared.INVALID) {
570 // We are collapsing a previously expanded media set
571 if (mediaSetIndex < mMediaSets.size() && mExpandedMediaSetIndex >= 0) {
572 MediaSet set = mMediaSets.get(mExpandedMediaSetIndex);
573 if (set.getNumItems() == 0) {
578 mExpandedMediaSetIndex = mediaSetIndex;
579 if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
580 // Notify Picasa that the user entered the album.
581 // MediaSet set = mMediaSets.get(mediaSetIndex);
582 // PicasaService.requestSync(mContext, PicasaService.TYPE_ALBUM_PHOTOS, set.mPicasaAlbumId);
584 updateListener(true);
585 mMediaFeedNeedsToRun = true;
588 public boolean canExpandSet(int slotIndex) {
589 int mediaSetIndex = slotIndex;
590 if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
591 MediaSet set = mMediaSets.get(mediaSetIndex);
592 if (set.getNumItems() > 0) {
593 MediaItem item = set.getItems().get(0);
594 if (item.mId == Shared.INVALID) {
603 public boolean hasExpandedMediaSet() {
604 return (mExpandedMediaSetIndex != Shared.INVALID);
607 public boolean restorePreviousClusteringState() {
608 boolean retVal = disableClusteringIfNecessary();
610 if (mListener != null) {
611 mListener.onFeedAboutToChange(this);
613 updateListener(true);
614 mMediaFeedNeedsToRun = true;
619 private boolean disableClusteringIfNecessary() {
620 if (mInClusteringMode) {
621 // Disable clustering.
622 mInClusteringMode = false;
623 mMediaFeedNeedsToRun = true;
629 public boolean isClustered() {
630 return mInClusteringMode;
633 public MediaSet getCurrentSet() {
634 if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
635 return mMediaSets.get(mExpandedMediaSetIndex);
640 public void performClustering() {
641 if (mListener != null) {
642 mListener.onFeedAboutToChange(this);
644 MediaSet setToUse = null;
645 if (mExpandedMediaSetIndex != Shared.INVALID || mExpandedMediaSetIndex < mMediaSets.size()) {
646 setToUse = mMediaSets.get(mExpandedMediaSetIndex);
648 if (setToUse != null) {
649 MediaClustering clustering = null;
650 synchronized (mClusterSets) {
651 // Make sure the computation is completed to the end.
652 clustering = mClusterSets.get(setToUse);
653 if (clustering != null) {
654 clustering.compute(null, true);
659 mInClusteringMode = true;
660 mMediaFeedNeedsToRun = true;
661 updateListener(true);
665 public void moveSetToFront(MediaSet mediaSet) {
666 ArrayList<MediaSet> mediaSets = mMediaSets;
667 int numSets = mediaSets.size();
669 mediaSets.add(mediaSet);
672 MediaSet setToFind = mediaSets.get(0);
673 if (setToFind == mediaSet) {
676 mediaSets.set(0, mediaSet);
677 int indexToSwapTill = -1;
678 for (int i = 1; i < numSets; ++i) {
679 MediaSet set = mediaSets.get(i);
680 if (set == mediaSet) {
681 mediaSets.set(i, setToFind);
686 if (indexToSwapTill != Shared.INVALID) {
687 for (int i = indexToSwapTill; i > 1; --i) {
688 MediaSet setEnd = mediaSets.get(i);
689 MediaSet setPrev = mediaSets.get(i - 1);
690 mediaSets.set(i, setPrev);
691 mediaSets.set(i - 1, setEnd);
694 mMediaFeedNeedsToRun = true;
697 public MediaSet replaceMediaSet(long setId, DataSource dataSource) {
698 MediaSet mediaSet = new MediaSet(dataSource);
699 mediaSet.mId = setId;
700 ArrayList<MediaSet> mediaSets = mMediaSets;
701 int numSets = mediaSets.size();
702 for (int i = 0; i < numSets; ++i) {
703 if (mediaSets.get(i).mId == setId) {
704 MediaSet thisSet = mediaSets.get(i);
705 mediaSet.mName = thisSet.mName;
706 mediaSets.set(i, mediaSet);
710 mMediaFeedNeedsToRun = true;
714 public void setSingleImageMode(boolean singleImageMode) {
715 mSingleImageMode = singleImageMode;
718 public boolean isSingleImageMode() {
719 return mSingleImageMode;
722 public MediaSet getExpandedMediaSet() {
723 if (mExpandedMediaSetIndex == Shared.INVALID)
725 if (mExpandedMediaSetIndex >= mMediaSets.size())
727 return mMediaSets.get(mExpandedMediaSetIndex);