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 if (Thread.interrupted())
360 mWaitingForMediaScanner = true;
362 if (mContext == null)
364 showToast(mContext.getResources().getString(R.string.initializing), Toast.LENGTH_LONG);
366 } catch (InterruptedException e) {
370 if (mWaitingForMediaScanner) {
371 showToast(mContext.getResources().getString(R.string.loading_new), Toast.LENGTH_LONG);
372 mWaitingForMediaScanner = false;
373 if (dataSource != null) {
374 dataSource.loadMediaSets(feed);
380 mAlbumSourceThread.setName("MediaSets");
381 mAlbumSourceThread.start();
384 private void showToast(final String string, final int duration) {
385 showToast(string, duration, false);
388 private void showToast(final String string, final int duration, final boolean centered) {
389 if (mContext != null && !((Gallery) mContext).isPaused()) {
390 ((Gallery) mContext).getHandler().post(new Runnable() {
392 if (mContext != null) {
393 Toast toast = Toast.makeText(mContext, string, duration);
395 toast.setGravity(Gravity.CENTER, 0, 0);
405 DataSource dataSource = mDataSource;
407 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
408 if (dataSource != null) {
409 while (!Thread.interrupted()) {
410 if (mListenerNeedsUpdate) {
411 mListenerNeedsUpdate = false;
412 if (mListener != null)
413 mListener.onFeedChanged(this, mListenerNeedsLayout);
415 Thread.sleep(sleepMs);
416 } catch (InterruptedException e) {
420 if (mWaitingForMediaScanner) {
421 synchronized (mMediaSets) {
426 Thread.sleep(sleepMs);
427 } catch (InterruptedException e) {
432 if (!mMediaFeedNeedsToRun)
434 if (((Gallery) mContext).isPaused())
436 mMediaFeedNeedsToRun = false;
437 ArrayList<MediaSet> mediaSets = mMediaSets;
438 synchronized (mediaSets) {
439 int expandedSetIndex = mExpandedMediaSetIndex;
440 if (expandedSetIndex >= mMediaSets.size()) {
441 expandedSetIndex = Shared.INVALID;
443 if (expandedSetIndex == Shared.INVALID) {
444 // We purge the sets outside this visibleRange.
445 int numSets = mediaSets.size();
446 IndexRange visibleRange = mVisibleRange;
447 IndexRange bufferedRange = mBufferedRange;
448 boolean scanMediaSets = true;
449 for (int i = 0; i < numSets; ++i) {
450 if (i >= visibleRange.begin && i <= visibleRange.end && scanMediaSets) {
451 MediaSet set = mediaSets.get(i);
452 int numItemsLoaded = set.mNumItemsLoaded;
453 if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
454 dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
455 if (set.getNumExpectedItems() == 0) {
456 mediaSets.remove(set);
459 if (mListener != null) {
460 mListener.onFeedChanged(this, false);
463 scanMediaSets = false;
465 if (!set.setContainsValidItems()) {
466 mediaSets.remove(set);
467 if (mListener != null) {
468 mListener.onFeedChanged(this, false);
474 numSets = mediaSets.size();
475 for (int i = 0; i < numSets; ++i) {
476 MediaSet set = mediaSets.get(i);
477 if (i >= bufferedRange.begin && i <= bufferedRange.end) {
479 int numItemsLoaded = set.mNumItemsLoaded;
480 if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
481 dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
482 if (set.getNumExpectedItems() == 0) {
483 mediaSets.remove(set);
486 if (mListener != null) {
487 mListener.onFeedChanged(this, false);
490 scanMediaSets = false;
493 } else if (i < bufferedRange.begin || i > bufferedRange.end) {
494 // Purge this set to its initial status.
495 MediaClustering clustering = mClusterSets.get(set);
496 if (clustering != null) {
498 mClusterSets.remove(set);
500 if (set.getNumItems() != 0)
505 if (expandedSetIndex != Shared.INVALID) {
506 int numSets = mMediaSets.size();
507 for (int i = 0; i < numSets; ++i) {
509 if (i != expandedSetIndex) {
510 MediaSet set = mediaSets.get(i);
511 MediaClustering clustering = mClusterSets.get(set);
512 if (clustering != null) {
514 mClusterSets.remove(set);
516 if (set.getNumItems() != 0)
520 // Make sure all the items are loaded for the album.
521 int numItemsLoaded = mediaSets.get(expandedSetIndex).mNumItemsLoaded;
522 int requestedItems = mVisibleRange.end;
523 // requestedItems count changes in clustering mode.
524 if (mInClusteringMode && mClusterSets != null) {
526 MediaClustering clustering = mClusterSets.get(mediaSets.get(expandedSetIndex));
527 if (clustering != null) {
528 ArrayList<Cluster> clusters = clustering.getClustersForDisplay();
529 int numClusters = clusters.size();
530 for (int i = 0; i < numClusters; i++) {
531 requestedItems += clusters.get(i).getNumExpectedItems();
535 MediaSet set = mediaSets.get(expandedSetIndex);
536 if (numItemsLoaded < set.getNumExpectedItems()) {
537 // We perform calculations for a window that gets anchored to a multiple of NUM_ITEMS_LOOKAHEAD.
538 // The start of the window is 0, x, 2x, 3x ... etc where x = NUM_ITEMS_LOOKAHEAD.
539 dataSource.loadItemsForSet(this, set, numItemsLoaded, (requestedItems / NUM_ITEMS_LOOKAHEAD)
540 * NUM_ITEMS_LOOKAHEAD + NUM_ITEMS_LOOKAHEAD);
541 if (set.getNumExpectedItems() == 0) {
542 mediaSets.remove(set);
543 mListener.onFeedChanged(this, false);
545 if (numItemsLoaded != set.mNumItemsLoaded && mListener != null) {
546 mListener.onFeedChanged(this, false);
550 MediaFilter filter = mMediaFilter;
551 if (filter != null && mMediaFilteredSet == null) {
552 if (expandedSetIndex != Shared.INVALID) {
553 MediaSet set = mediaSets.get(expandedSetIndex);
554 ArrayList<MediaItem> items = set.getItems();
555 int numItems = set.getNumItems();
556 MediaSet filteredSet = new MediaSet();
557 filteredSet.setNumExpectedItems(numItems);
558 mMediaFilteredSet = filteredSet;
559 for (int i = 0; i < numItems; ++i) {
560 MediaItem item = items.get(i);
561 if (filter.pass(item)) {
562 filteredSet.addItem(item);
565 filteredSet.updateNumExpectedItems();
566 filteredSet.generateTitle(true);
568 updateListener(true);
575 public void expandMediaSet(int mediaSetIndex) {
576 // We need to check if this slot can be focused or not.
577 if (mListener != null) {
578 mListener.onFeedAboutToChange(this);
580 if (mExpandedMediaSetIndex > 0 && mediaSetIndex == Shared.INVALID) {
581 // We are collapsing a previously expanded media set
582 if (mediaSetIndex < mMediaSets.size() && mExpandedMediaSetIndex >= 0 && mExpandedMediaSetIndex < mMediaSets.size()) {
583 MediaSet set = mMediaSets.get(mExpandedMediaSetIndex);
584 if (set.getNumItems() == 0) {
589 mExpandedMediaSetIndex = mediaSetIndex;
590 if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
591 // Notify Picasa that the user entered the album.
592 // MediaSet set = mMediaSets.get(mediaSetIndex);
593 // PicasaService.requestSync(mContext,
594 // PicasaService.TYPE_ALBUM_PHOTOS, set.mPicasaAlbumId);
596 updateListener(true);
597 mMediaFeedNeedsToRun = true;
600 public boolean canExpandSet(int slotIndex) {
601 int mediaSetIndex = slotIndex;
602 if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
603 MediaSet set = mMediaSets.get(mediaSetIndex);
604 if (set.getNumItems() > 0) {
605 MediaItem item = set.getItems().get(0);
606 if (item.mId == Shared.INVALID) {
615 public boolean hasExpandedMediaSet() {
616 return (mExpandedMediaSetIndex != Shared.INVALID);
619 public boolean restorePreviousClusteringState() {
620 boolean retVal = disableClusteringIfNecessary();
622 if (mListener != null) {
623 mListener.onFeedAboutToChange(this);
625 updateListener(true);
626 mMediaFeedNeedsToRun = true;
631 private boolean disableClusteringIfNecessary() {
632 if (mInClusteringMode) {
633 // Disable clustering.
634 mInClusteringMode = false;
635 mMediaFeedNeedsToRun = true;
641 public boolean isClustered() {
642 return mInClusteringMode;
645 public MediaSet getCurrentSet() {
646 if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
647 return mMediaSets.get(mExpandedMediaSetIndex);
652 public void performClustering() {
653 if (mListener != null) {
654 mListener.onFeedAboutToChange(this);
656 MediaSet setToUse = null;
657 if (mExpandedMediaSetIndex != Shared.INVALID || mExpandedMediaSetIndex < mMediaSets.size()) {
658 setToUse = mMediaSets.get(mExpandedMediaSetIndex);
660 if (setToUse != null) {
661 MediaClustering clustering = null;
662 synchronized (mClusterSets) {
663 // Make sure the computation is completed to the end.
664 clustering = mClusterSets.get(setToUse);
665 if (clustering != null) {
666 clustering.compute(null, true);
671 mInClusteringMode = true;
672 mMediaFeedNeedsToRun = true;
673 updateListener(true);
677 public void moveSetToFront(MediaSet mediaSet) {
678 ArrayList<MediaSet> mediaSets = mMediaSets;
679 int numSets = mediaSets.size();
681 mediaSets.add(mediaSet);
684 MediaSet setToFind = mediaSets.get(0);
685 if (setToFind == mediaSet) {
688 mediaSets.set(0, mediaSet);
689 int indexToSwapTill = -1;
690 for (int i = 1; i < numSets; ++i) {
691 MediaSet set = mediaSets.get(i);
692 if (set == mediaSet) {
693 mediaSets.set(i, setToFind);
698 if (indexToSwapTill != Shared.INVALID) {
699 for (int i = indexToSwapTill; i > 1; --i) {
700 MediaSet setEnd = mediaSets.get(i);
701 MediaSet setPrev = mediaSets.get(i - 1);
702 mediaSets.set(i, setPrev);
703 mediaSets.set(i - 1, setEnd);
706 mMediaFeedNeedsToRun = true;
709 public MediaSet replaceMediaSet(long setId, DataSource dataSource) {
710 MediaSet mediaSet = new MediaSet(dataSource);
711 mediaSet.mId = setId;
712 ArrayList<MediaSet> mediaSets = mMediaSets;
713 int numSets = mediaSets.size();
714 for (int i = 0; i < numSets; ++i) {
715 final MediaSet thisSet = mediaSets.get(i);
716 if (thisSet.mId == setId) {
717 mediaSet.mName = thisSet.mName;
718 mediaSet.mHasImages = thisSet.mHasImages;
719 mediaSet.mHasVideos = thisSet.mHasVideos;
720 mediaSets.set(i, mediaSet);
724 mMediaFeedNeedsToRun = true;
728 public void setSingleImageMode(boolean singleImageMode) {
729 mSingleImageMode = singleImageMode;
732 public boolean isSingleImageMode() {
733 return mSingleImageMode;
736 public MediaSet getExpandedMediaSet() {
737 if (mExpandedMediaSetIndex == Shared.INVALID)
739 if (mExpandedMediaSetIndex >= mMediaSets.size())
741 return mMediaSets.get(mExpandedMediaSetIndex);