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
219 MediaClustering clustering = mClusterSets.get(set);
220 for (int j = 0; j < numItems; ++j) {
221 MediaItem item = items.get(j);
222 removeItemFromMediaSet(item, set);
223 if (clustering != null) {
224 clustering.removeItemFromClustering(item);
227 set.updateNumExpectedItems();
228 set.generateTitle(true);
231 updateListener(true);
232 mMediaFeedNeedsToRun = true;
233 if (mDataSource != null) {
234 mDataSource.performOperation(OPERATION_DELETE, mediaBuckets, null);
237 mDataSource.performOperation(operation, mediaBuckets, data);
241 operationThread.setName("Operation " + operation);
242 operationThread.start();
245 public void removeMediaSet(MediaSet set) {
246 synchronized (mMediaSets) {
247 mMediaSets.remove(set);
249 mMediaFeedNeedsToRun = true;
252 private void removeItemFromMediaSet(MediaItem item, MediaSet mediaSet) {
253 mediaSet.removeItem(item);
254 synchronized (mClusterSets) {
255 MediaClustering clustering = mClusterSets.get(mediaSet);
256 if (clustering != null) {
257 clustering.removeItemFromClustering(item);
260 mMediaFeedNeedsToRun = true;
263 public void updateListener(boolean needsLayout) {
264 mListenerNeedsUpdate = true;
265 mListenerNeedsLayout = needsLayout;
268 public int getNumSlots() {
269 int currentMediaSetIndex = mExpandedMediaSetIndex;
270 ArrayList<MediaSet> mediaSets = mMediaSets;
271 int mediaSetsSize = mediaSets.size();
273 if (mInClusteringMode == false) {
274 if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize) {
275 return mediaSetsSize;
277 MediaSet setToUse = (mMediaFilteredSet == null) ? mediaSets.get(currentMediaSetIndex) : mMediaFilteredSet;
278 return setToUse.getNumItems();
280 } else if (currentMediaSetIndex != Shared.INVALID && currentMediaSetIndex < mediaSetsSize) {
281 MediaSet set = mediaSets.get(currentMediaSetIndex);
282 MediaClustering clustering = mClusterSets.get(set);
283 if (clustering != null) {
284 return clustering.getClustersForDisplay().size();
290 public MediaSet getSetForSlot(int slotIndex) {
295 ArrayList<MediaSet> mediaSets = mMediaSets;
296 int mediaSetsSize = mediaSets.size();
297 int currentMediaSetIndex = mExpandedMediaSetIndex;
299 if (mInClusteringMode == false) {
300 if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize) {
301 if (slotIndex >= mediaSetsSize) {
304 return mMediaSets.get(slotIndex);
306 if (mSingleWrapper.getNumItems() == 0) {
307 mSingleWrapper.addItem(null);
309 MediaSet setToUse = (mMediaFilteredSet == null) ? mMediaSets.get(currentMediaSetIndex) : mMediaFilteredSet;
310 ArrayList<MediaItem> items = setToUse.getItems();
311 if (slotIndex >= setToUse.getNumItems()) {
314 mSingleWrapper.getItems().set(0, items.get(slotIndex));
315 return mSingleWrapper;
316 } else if (currentMediaSetIndex != Shared.INVALID && currentMediaSetIndex < mediaSetsSize) {
317 MediaSet set = mediaSets.get(currentMediaSetIndex);
318 MediaClustering clustering = mClusterSets.get(set);
319 if (clustering != null) {
320 ArrayList<MediaClustering.Cluster> clusters = clustering.getClustersForDisplay();
321 if (clusters.size() > slotIndex) {
322 MediaClustering.Cluster cluster = clusters.get(slotIndex);
323 cluster.generateCaption(mContext);
331 public boolean getWaitingForMediaScanner() {
332 return mWaitingForMediaScanner;
335 public boolean isLoading() {
339 public void start() {
340 final MediaFeed feed = this;
342 mDataSourceThread = new Thread(this);
343 mDataSourceThread.setName("MediaFeed");
344 mAlbumSourceThread = new Thread(new Runnable() {
346 if (mContext == null)
348 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
349 DataSource dataSource = mDataSource;
350 // We must wait while the SD card is mounted or the MediaScanner
352 if (dataSource != null) {
353 dataSource.loadMediaSets(feed);
355 mWaitingForMediaScanner = false;
356 while (ImageManager.isMediaScannerScanning(mContext.getContentResolver())) {
357 // MediaScanner is still running, wait
358 mWaitingForMediaScanner = true;
360 if (mContext == null)
362 showToast(mContext.getResources().getString(R.string.initializing), Toast.LENGTH_LONG);
364 } catch (InterruptedException e) {
368 if (mWaitingForMediaScanner) {
369 showToast(mContext.getResources().getString(R.string.loading_new), Toast.LENGTH_LONG);
370 mWaitingForMediaScanner = false;
371 if (dataSource != null) {
372 dataSource.loadMediaSets(feed);
378 mAlbumSourceThread.setName("MediaSets");
379 mAlbumSourceThread.start();
382 private void showToast(final String string, final int duration) {
383 showToast(string, duration, false);
386 private void showToast(final String string, final int duration, final boolean centered) {
387 if (mContext != null && !((Gallery) mContext).isPaused()) {
388 ((Gallery) mContext).getHandler().post(new Runnable() {
390 if (mContext != null) {
391 Toast toast = Toast.makeText(mContext, string, duration);
393 toast.setGravity(Gravity.CENTER, 0, 0);
403 DataSource dataSource = mDataSource;
405 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
406 if (dataSource != null) {
407 while (!Thread.interrupted()) {
408 if (mListenerNeedsUpdate) {
409 mListenerNeedsUpdate = false;
410 if (mListener != null)
411 mListener.onFeedChanged(this, mListenerNeedsLayout);
413 Thread.sleep(sleepMs);
414 } catch (InterruptedException e) {
418 if (mWaitingForMediaScanner) {
419 synchronized (mMediaSets) {
424 Thread.sleep(sleepMs);
425 } catch (InterruptedException e) {
430 if (!mMediaFeedNeedsToRun)
432 mMediaFeedNeedsToRun = false;
433 ArrayList<MediaSet> mediaSets = mMediaSets;
434 synchronized (mediaSets) {
435 int expandedSetIndex = mExpandedMediaSetIndex;
436 if (expandedSetIndex >= mMediaSets.size()) {
437 expandedSetIndex = Shared.INVALID;
439 if (expandedSetIndex == Shared.INVALID) {
440 // We purge the sets outside this visibleRange.
441 int numSets = mediaSets.size();
442 IndexRange visibleRange = mVisibleRange;
443 IndexRange bufferedRange = mBufferedRange;
444 boolean scanMediaSets = true;
445 for (int i = 0; i < numSets; ++i) {
446 if (i >= visibleRange.begin && i <= visibleRange.end && scanMediaSets) {
447 MediaSet set = mediaSets.get(i);
448 int numItemsLoaded = set.mNumItemsLoaded;
449 if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
450 dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
451 if (set.getNumExpectedItems() == 0) {
452 mediaSets.remove(set);
455 if (mListener != null) {
456 mListener.onFeedChanged(this, false);
459 scanMediaSets = false;
461 if (!set.setContainsValidItems()) {
462 mediaSets.remove(set);
463 if (mListener != null) {
464 mListener.onFeedChanged(this, false);
470 numSets = mediaSets.size();
471 for (int i = 0; i < numSets; ++i) {
472 MediaSet set = mediaSets.get(i);
473 if (i >= bufferedRange.begin && i <= bufferedRange.end) {
475 int numItemsLoaded = set.mNumItemsLoaded;
476 if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
477 dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
478 if (set.getNumExpectedItems() == 0) {
479 mediaSets.remove(set);
482 if (mListener != null) {
483 mListener.onFeedChanged(this, false);
486 scanMediaSets = false;
489 } else if (i < bufferedRange.begin || i > bufferedRange.end) {
490 // Purge this set to its initial status.
491 MediaClustering clustering = mClusterSets.get(set);
492 if (clustering != null) {
494 mClusterSets.remove(set);
496 if (set.getNumItems() != 0)
501 if (expandedSetIndex != Shared.INVALID) {
502 int numSets = mMediaSets.size();
503 for (int i = 0; i < numSets; ++i) {
505 if (i != expandedSetIndex) {
506 MediaSet set = mediaSets.get(i);
507 MediaClustering clustering = mClusterSets.get(set);
508 if (clustering != null) {
510 mClusterSets.remove(set);
512 if (set.getNumItems() != 0)
516 // Make sure all the items are loaded for the album.
517 int numItemsLoaded = mediaSets.get(expandedSetIndex).mNumItemsLoaded;
518 int requestedItems = mVisibleRange.end;
519 // requestedItems count changes in clustering mode.
520 if (mInClusteringMode && mClusterSets != null) {
522 MediaClustering clustering = mClusterSets.get(mediaSets.get(expandedSetIndex));
523 if (clustering != null) {
524 ArrayList<Cluster> clusters = clustering.getClustersForDisplay();
525 int numClusters = clusters.size();
526 for (int i = 0; i < numClusters; i++) {
527 requestedItems += clusters.get(i).getNumExpectedItems();
531 MediaSet set = mediaSets.get(expandedSetIndex);
532 if (numItemsLoaded < set.getNumExpectedItems()) {
533 // We perform calculations for a window that gets anchored to a multiple of NUM_ITEMS_LOOKAHEAD.
534 // The start of the window is 0, x, 2x, 3x ... etc where x = NUM_ITEMS_LOOKAHEAD.
535 dataSource.loadItemsForSet(this, set, numItemsLoaded, (requestedItems / NUM_ITEMS_LOOKAHEAD)
536 * NUM_ITEMS_LOOKAHEAD + NUM_ITEMS_LOOKAHEAD);
537 if (set.getNumExpectedItems() == 0) {
538 mediaSets.remove(set);
539 mListener.onFeedChanged(this, false);
541 if (numItemsLoaded != set.mNumItemsLoaded && mListener != null) {
542 mListener.onFeedChanged(this, false);
546 MediaFilter filter = mMediaFilter;
547 if (filter != null && mMediaFilteredSet == null) {
548 if (expandedSetIndex != Shared.INVALID) {
549 MediaSet set = mediaSets.get(expandedSetIndex);
550 ArrayList<MediaItem> items = set.getItems();
551 int numItems = set.getNumItems();
552 MediaSet filteredSet = new MediaSet();
553 filteredSet.setNumExpectedItems(numItems);
554 mMediaFilteredSet = filteredSet;
555 for (int i = 0; i < numItems; ++i) {
556 MediaItem item = items.get(i);
557 if (filter.pass(item)) {
558 filteredSet.addItem(item);
561 filteredSet.updateNumExpectedItems();
562 filteredSet.generateTitle(true);
564 updateListener(true);
571 public void expandMediaSet(int mediaSetIndex) {
572 // We need to check if this slot can be focused or not.
573 if (mListener != null) {
574 mListener.onFeedAboutToChange(this);
576 if (mExpandedMediaSetIndex > 0 && mediaSetIndex == Shared.INVALID) {
577 // We are collapsing a previously expanded media set
578 if (mediaSetIndex < mMediaSets.size() && mExpandedMediaSetIndex >= 0 && mExpandedMediaSetIndex < mMediaSets.size()) {
579 MediaSet set = mMediaSets.get(mExpandedMediaSetIndex);
580 if (set.getNumItems() == 0) {
585 mExpandedMediaSetIndex = mediaSetIndex;
586 if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
587 // Notify Picasa that the user entered the album.
588 // MediaSet set = mMediaSets.get(mediaSetIndex);
589 // PicasaService.requestSync(mContext,
590 // PicasaService.TYPE_ALBUM_PHOTOS, set.mPicasaAlbumId);
592 updateListener(true);
593 mMediaFeedNeedsToRun = true;
596 public boolean canExpandSet(int slotIndex) {
597 int mediaSetIndex = slotIndex;
598 if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
599 MediaSet set = mMediaSets.get(mediaSetIndex);
600 if (set.getNumItems() > 0) {
601 MediaItem item = set.getItems().get(0);
602 if (item.mId == Shared.INVALID) {
611 public boolean hasExpandedMediaSet() {
612 return (mExpandedMediaSetIndex != Shared.INVALID);
615 public boolean restorePreviousClusteringState() {
616 boolean retVal = disableClusteringIfNecessary();
618 if (mListener != null) {
619 mListener.onFeedAboutToChange(this);
621 updateListener(true);
622 mMediaFeedNeedsToRun = true;
627 private boolean disableClusteringIfNecessary() {
628 if (mInClusteringMode) {
629 // Disable clustering.
630 mInClusteringMode = false;
631 mMediaFeedNeedsToRun = true;
637 public boolean isClustered() {
638 return mInClusteringMode;
641 public MediaSet getCurrentSet() {
642 if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
643 return mMediaSets.get(mExpandedMediaSetIndex);
648 public void performClustering() {
649 if (mListener != null) {
650 mListener.onFeedAboutToChange(this);
652 MediaSet setToUse = null;
653 if (mExpandedMediaSetIndex != Shared.INVALID || mExpandedMediaSetIndex < mMediaSets.size()) {
654 setToUse = mMediaSets.get(mExpandedMediaSetIndex);
656 if (setToUse != null) {
657 MediaClustering clustering = null;
658 synchronized (mClusterSets) {
659 // Make sure the computation is completed to the end.
660 clustering = mClusterSets.get(setToUse);
661 if (clustering != null) {
662 clustering.compute(null, true);
667 mInClusteringMode = true;
668 mMediaFeedNeedsToRun = true;
669 updateListener(true);
673 public void moveSetToFront(MediaSet mediaSet) {
674 ArrayList<MediaSet> mediaSets = mMediaSets;
675 int numSets = mediaSets.size();
677 mediaSets.add(mediaSet);
680 MediaSet setToFind = mediaSets.get(0);
681 if (setToFind == mediaSet) {
684 mediaSets.set(0, mediaSet);
685 int indexToSwapTill = -1;
686 for (int i = 1; i < numSets; ++i) {
687 MediaSet set = mediaSets.get(i);
688 if (set == mediaSet) {
689 mediaSets.set(i, setToFind);
694 if (indexToSwapTill != Shared.INVALID) {
695 for (int i = indexToSwapTill; i > 1; --i) {
696 MediaSet setEnd = mediaSets.get(i);
697 MediaSet setPrev = mediaSets.get(i - 1);
698 mediaSets.set(i, setPrev);
699 mediaSets.set(i - 1, setEnd);
702 mMediaFeedNeedsToRun = true;
705 public MediaSet replaceMediaSet(long setId, DataSource dataSource) {
706 MediaSet mediaSet = new MediaSet(dataSource);
707 mediaSet.mId = setId;
708 ArrayList<MediaSet> mediaSets = mMediaSets;
709 int numSets = mediaSets.size();
710 for (int i = 0; i < numSets; ++i) {
711 final MediaSet thisSet = mediaSets.get(i);
712 if (thisSet.mId == setId) {
713 mediaSet.mName = thisSet.mName;
714 mediaSet.mHasImages = thisSet.mHasImages;
715 mediaSet.mHasVideos = thisSet.mHasVideos;
716 mediaSets.set(i, mediaSet);
720 mMediaFeedNeedsToRun = true;
724 public void setSingleImageMode(boolean singleImageMode) {
725 mSingleImageMode = singleImageMode;
728 public boolean isSingleImageMode() {
729 return mSingleImageMode;
732 public MediaSet getExpandedMediaSet() {
733 if (mExpandedMediaSetIndex == Shared.INVALID)
735 if (mExpandedMediaSetIndex >= mMediaSets.size())
737 return mMediaSets.get(mExpandedMediaSetIndex);