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 mMediaFeedNeedsToRun = false;
435 ArrayList<MediaSet> mediaSets = mMediaSets;
436 synchronized (mediaSets) {
437 int expandedSetIndex = mExpandedMediaSetIndex;
438 if (expandedSetIndex >= mMediaSets.size()) {
439 expandedSetIndex = Shared.INVALID;
441 if (expandedSetIndex == Shared.INVALID) {
442 // We purge the sets outside this visibleRange.
443 int numSets = mediaSets.size();
444 IndexRange visibleRange = mVisibleRange;
445 IndexRange bufferedRange = mBufferedRange;
446 boolean scanMediaSets = true;
447 for (int i = 0; i < numSets; ++i) {
448 if (i >= visibleRange.begin && i <= visibleRange.end && scanMediaSets) {
449 MediaSet set = mediaSets.get(i);
450 int numItemsLoaded = set.mNumItemsLoaded;
451 if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
452 dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
453 if (set.getNumExpectedItems() == 0) {
454 mediaSets.remove(set);
457 if (mListener != null) {
458 mListener.onFeedChanged(this, false);
461 scanMediaSets = false;
463 if (!set.setContainsValidItems()) {
464 mediaSets.remove(set);
465 if (mListener != null) {
466 mListener.onFeedChanged(this, false);
472 numSets = mediaSets.size();
473 for (int i = 0; i < numSets; ++i) {
474 MediaSet set = mediaSets.get(i);
475 if (i >= bufferedRange.begin && i <= bufferedRange.end) {
477 int numItemsLoaded = set.mNumItemsLoaded;
478 if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
479 dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
480 if (set.getNumExpectedItems() == 0) {
481 mediaSets.remove(set);
484 if (mListener != null) {
485 mListener.onFeedChanged(this, false);
488 scanMediaSets = false;
491 } else if (i < bufferedRange.begin || i > bufferedRange.end) {
492 // Purge this set to its initial status.
493 MediaClustering clustering = mClusterSets.get(set);
494 if (clustering != null) {
496 mClusterSets.remove(set);
498 if (set.getNumItems() != 0)
503 if (expandedSetIndex != Shared.INVALID) {
504 int numSets = mMediaSets.size();
505 for (int i = 0; i < numSets; ++i) {
507 if (i != expandedSetIndex) {
508 MediaSet set = mediaSets.get(i);
509 MediaClustering clustering = mClusterSets.get(set);
510 if (clustering != null) {
512 mClusterSets.remove(set);
514 if (set.getNumItems() != 0)
518 // Make sure all the items are loaded for the album.
519 int numItemsLoaded = mediaSets.get(expandedSetIndex).mNumItemsLoaded;
520 int requestedItems = mVisibleRange.end;
521 // requestedItems count changes in clustering mode.
522 if (mInClusteringMode && mClusterSets != null) {
524 MediaClustering clustering = mClusterSets.get(mediaSets.get(expandedSetIndex));
525 if (clustering != null) {
526 ArrayList<Cluster> clusters = clustering.getClustersForDisplay();
527 int numClusters = clusters.size();
528 for (int i = 0; i < numClusters; i++) {
529 requestedItems += clusters.get(i).getNumExpectedItems();
533 MediaSet set = mediaSets.get(expandedSetIndex);
534 if (numItemsLoaded < set.getNumExpectedItems()) {
535 // We perform calculations for a window that gets anchored to a multiple of NUM_ITEMS_LOOKAHEAD.
536 // The start of the window is 0, x, 2x, 3x ... etc where x = NUM_ITEMS_LOOKAHEAD.
537 dataSource.loadItemsForSet(this, set, numItemsLoaded, (requestedItems / NUM_ITEMS_LOOKAHEAD)
538 * NUM_ITEMS_LOOKAHEAD + NUM_ITEMS_LOOKAHEAD);
539 if (set.getNumExpectedItems() == 0) {
540 mediaSets.remove(set);
541 mListener.onFeedChanged(this, false);
543 if (numItemsLoaded != set.mNumItemsLoaded && mListener != null) {
544 mListener.onFeedChanged(this, false);
548 MediaFilter filter = mMediaFilter;
549 if (filter != null && mMediaFilteredSet == null) {
550 if (expandedSetIndex != Shared.INVALID) {
551 MediaSet set = mediaSets.get(expandedSetIndex);
552 ArrayList<MediaItem> items = set.getItems();
553 int numItems = set.getNumItems();
554 MediaSet filteredSet = new MediaSet();
555 filteredSet.setNumExpectedItems(numItems);
556 mMediaFilteredSet = filteredSet;
557 for (int i = 0; i < numItems; ++i) {
558 MediaItem item = items.get(i);
559 if (filter.pass(item)) {
560 filteredSet.addItem(item);
563 filteredSet.updateNumExpectedItems();
564 filteredSet.generateTitle(true);
566 updateListener(true);
573 public void expandMediaSet(int mediaSetIndex) {
574 // We need to check if this slot can be focused or not.
575 if (mListener != null) {
576 mListener.onFeedAboutToChange(this);
578 if (mExpandedMediaSetIndex > 0 && mediaSetIndex == Shared.INVALID) {
579 // We are collapsing a previously expanded media set
580 if (mediaSetIndex < mMediaSets.size() && mExpandedMediaSetIndex >= 0 && mExpandedMediaSetIndex < mMediaSets.size()) {
581 MediaSet set = mMediaSets.get(mExpandedMediaSetIndex);
582 if (set.getNumItems() == 0) {
587 mExpandedMediaSetIndex = mediaSetIndex;
588 if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
589 // Notify Picasa that the user entered the album.
590 // MediaSet set = mMediaSets.get(mediaSetIndex);
591 // PicasaService.requestSync(mContext,
592 // PicasaService.TYPE_ALBUM_PHOTOS, set.mPicasaAlbumId);
594 updateListener(true);
595 mMediaFeedNeedsToRun = true;
598 public boolean canExpandSet(int slotIndex) {
599 int mediaSetIndex = slotIndex;
600 if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
601 MediaSet set = mMediaSets.get(mediaSetIndex);
602 if (set.getNumItems() > 0) {
603 MediaItem item = set.getItems().get(0);
604 if (item.mId == Shared.INVALID) {
613 public boolean hasExpandedMediaSet() {
614 return (mExpandedMediaSetIndex != Shared.INVALID);
617 public boolean restorePreviousClusteringState() {
618 boolean retVal = disableClusteringIfNecessary();
620 if (mListener != null) {
621 mListener.onFeedAboutToChange(this);
623 updateListener(true);
624 mMediaFeedNeedsToRun = true;
629 private boolean disableClusteringIfNecessary() {
630 if (mInClusteringMode) {
631 // Disable clustering.
632 mInClusteringMode = false;
633 mMediaFeedNeedsToRun = true;
639 public boolean isClustered() {
640 return mInClusteringMode;
643 public MediaSet getCurrentSet() {
644 if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
645 return mMediaSets.get(mExpandedMediaSetIndex);
650 public void performClustering() {
651 if (mListener != null) {
652 mListener.onFeedAboutToChange(this);
654 MediaSet setToUse = null;
655 if (mExpandedMediaSetIndex != Shared.INVALID || mExpandedMediaSetIndex < mMediaSets.size()) {
656 setToUse = mMediaSets.get(mExpandedMediaSetIndex);
658 if (setToUse != null) {
659 MediaClustering clustering = null;
660 synchronized (mClusterSets) {
661 // Make sure the computation is completed to the end.
662 clustering = mClusterSets.get(setToUse);
663 if (clustering != null) {
664 clustering.compute(null, true);
669 mInClusteringMode = true;
670 mMediaFeedNeedsToRun = true;
671 updateListener(true);
675 public void moveSetToFront(MediaSet mediaSet) {
676 ArrayList<MediaSet> mediaSets = mMediaSets;
677 int numSets = mediaSets.size();
679 mediaSets.add(mediaSet);
682 MediaSet setToFind = mediaSets.get(0);
683 if (setToFind == mediaSet) {
686 mediaSets.set(0, mediaSet);
687 int indexToSwapTill = -1;
688 for (int i = 1; i < numSets; ++i) {
689 MediaSet set = mediaSets.get(i);
690 if (set == mediaSet) {
691 mediaSets.set(i, setToFind);
696 if (indexToSwapTill != Shared.INVALID) {
697 for (int i = indexToSwapTill; i > 1; --i) {
698 MediaSet setEnd = mediaSets.get(i);
699 MediaSet setPrev = mediaSets.get(i - 1);
700 mediaSets.set(i, setPrev);
701 mediaSets.set(i - 1, setEnd);
704 mMediaFeedNeedsToRun = true;
707 public MediaSet replaceMediaSet(long setId, DataSource dataSource) {
708 MediaSet mediaSet = new MediaSet(dataSource);
709 mediaSet.mId = setId;
710 ArrayList<MediaSet> mediaSets = mMediaSets;
711 int numSets = mediaSets.size();
712 for (int i = 0; i < numSets; ++i) {
713 final MediaSet thisSet = mediaSets.get(i);
714 if (thisSet.mId == setId) {
715 mediaSet.mName = thisSet.mName;
716 mediaSet.mHasImages = thisSet.mHasImages;
717 mediaSet.mHasVideos = thisSet.mHasVideos;
718 mediaSets.set(i, mediaSet);
722 mMediaFeedNeedsToRun = true;
726 public void setSingleImageMode(boolean singleImageMode) {
727 mSingleImageMode = singleImageMode;
730 public boolean isSingleImageMode() {
731 return mSingleImageMode;
734 public MediaSet getExpandedMediaSet() {
735 if (mExpandedMediaSetIndex == Shared.INVALID)
737 if (mExpandedMediaSetIndex >= mMediaSets.size())
739 return mMediaSets.get(mExpandedMediaSetIndex);