/* * Copyright (C) 2010 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.android.gallery3d.data; import com.android.gallery3d.common.Utils; import com.android.gallery3d.util.Future; import java.util.ArrayList; import java.util.WeakHashMap; // MediaSet is a directory-like data structure. // It contains MediaItems and sub-MediaSets. // // The primary interface are: // getMediaItemCount(), getMediaItem() and // getSubMediaSetCount(), getSubMediaSet(). // // getTotalMediaItemCount() returns the number of all MediaItems, including // those in sub-MediaSets. public abstract class MediaSet extends MediaObject { @SuppressWarnings("unused") private static final String TAG = "MediaSet"; public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500; public static final int INDEX_NOT_FOUND = -1; public static final int SYNC_RESULT_SUCCESS = 0; public static final int SYNC_RESULT_CANCELLED = 1; public static final int SYNC_RESULT_ERROR = 2; /** Listener to be used with requestSync(SyncListener). */ public static interface SyncListener { /** * Called when the sync task completed. Completion may be due to normal termination, * an exception, or cancellation. * * @param mediaSet the MediaSet that's done with sync * @param resultCode one of the SYNC_RESULT_* constants */ void onSyncDone(MediaSet mediaSet, int resultCode); } public MediaSet(Path path, long version) { super(path, version); } public int getMediaItemCount() { return 0; } // Returns the media items in the range [start, start + count). // // The number of media items returned may be less than the specified count // if there are not enough media items available. The number of // media items available may not be consistent with the return value of // getMediaItemCount() because the contents of database may have already // changed. public ArrayList getMediaItem(int start, int count) { return new ArrayList(); } public MediaItem getCoverMediaItem() { ArrayList items = getMediaItem(0, 1); if (items.size() > 0) return items.get(0); for (int i = 0, n = getSubMediaSetCount(); i < n; i++) { MediaItem cover = getSubMediaSet(i).getCoverMediaItem(); if (cover != null) return cover; } return null; } public int getSubMediaSetCount() { return 0; } public MediaSet getSubMediaSet(int index) { throw new IndexOutOfBoundsException(); } public boolean isLeafAlbum() { return false; } /** * Method {@link #reload()} may process the loading task in background, this method tells * its client whether the loading is still in process or not. */ public boolean isLoading() { return false; } public int getTotalMediaItemCount() { int total = getMediaItemCount(); for (int i = 0, n = getSubMediaSetCount(); i < n; i++) { total += getSubMediaSet(i).getTotalMediaItemCount(); } return total; } // TODO: we should have better implementation of sub classes public int getIndexOfItem(Path path, int hint) { // hint < 0 is handled below // first, try to find it around the hint int start = Math.max(0, hint - MEDIAITEM_BATCH_FETCH_COUNT / 2); ArrayList list = getMediaItem( start, MEDIAITEM_BATCH_FETCH_COUNT); int index = getIndexOf(path, list); if (index != INDEX_NOT_FOUND) return start + index; // try to find it globally start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0; list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); while (true) { index = getIndexOf(path, list); if (index != INDEX_NOT_FOUND) return start + index; if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND; start += MEDIAITEM_BATCH_FETCH_COUNT; list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); } } protected int getIndexOf(Path path, ArrayList list) { for (int i = 0, n = list.size(); i < n; ++i) { // item could be null only in ClusterAlbum MediaObject item = list.get(i); if (item != null && item.mPath == path) return i; } return INDEX_NOT_FOUND; } public abstract String getName(); private WeakHashMap mListeners = new WeakHashMap(); // NOTE: The MediaSet only keeps a weak reference to the listener. The // listener is automatically removed when there is no other reference to // the listener. public void addContentListener(ContentListener listener) { if (mListeners.containsKey(listener)) { throw new IllegalArgumentException(); } mListeners.put(listener, null); } public void removeContentListener(ContentListener listener) { if (!mListeners.containsKey(listener)) { throw new IllegalArgumentException(); } mListeners.remove(listener); } // This should be called by subclasses when the content is changed. public void notifyContentChanged() { for (ContentListener listener : mListeners.keySet()) { listener.onContentDirty(); } } // Reload the content. Return the current data version. reload() should be called // in the same thread as getMediaItem(int, int) and getSubMediaSet(int). public abstract long reload(); @Override public MediaDetails getDetails() { MediaDetails details = super.getDetails(); details.addDetail(MediaDetails.INDEX_TITLE, getName()); return details; } // Enumerate all media items in this media set (including the ones in sub // media sets), in an efficient order. ItemConsumer.consumer() will be // called for each media item with its index. public void enumerateMediaItems(ItemConsumer consumer) { enumerateMediaItems(consumer, 0); } public void enumerateTotalMediaItems(ItemConsumer consumer) { enumerateTotalMediaItems(consumer, 0); } public static interface ItemConsumer { void consume(int index, MediaItem item); } // The default implementation uses getMediaItem() for enumerateMediaItems(). // Subclasses may override this and use more efficient implementations. // Returns the number of items enumerated. protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { int total = getMediaItemCount(); int start = 0; while (start < total) { int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start); ArrayList items = getMediaItem(start, count); for (int i = 0, n = items.size(); i < n; i++) { MediaItem item = items.get(i); consumer.consume(startIndex + start + i, item); } start += count; } return total; } // Recursively enumerate all media items under this set. // Returns the number of items enumerated. protected int enumerateTotalMediaItems( ItemConsumer consumer, int startIndex) { int start = 0; start += enumerateMediaItems(consumer, startIndex); int m = getSubMediaSetCount(); for (int i = 0; i < m; i++) { start += getSubMediaSet(i).enumerateTotalMediaItems( consumer, startIndex + start); } return start; } /** * Requests sync on this MediaSet. It returns a Future object that can be used by the caller * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants * defined in this class and can be obtained by Future.get(). * * Subclasses should perform sync on a different thread. * * The default implementation here returns a Future stub that does nothing and returns * SYNC_RESULT_SUCCESS by get(). */ public Future requestSync(SyncListener listener) { listener.onSyncDone(this, SYNC_RESULT_SUCCESS); return FUTURE_STUB; } private static final Future FUTURE_STUB = new Future() { @Override public void cancel() {} @Override public boolean isCancelled() { return false; } @Override public boolean isDone() { return true; } @Override public Integer get() { return SYNC_RESULT_SUCCESS; } @Override public void waitDone() {} }; protected Future requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) { return new MultiSetSyncFuture(sets, listener); } private class MultiSetSyncFuture implements Future, SyncListener { @SuppressWarnings("hiding") private static final String TAG = "Gallery.MultiSetSync"; private final SyncListener mListener; private final Future mFutures[]; private boolean mIsCancelled = false; private int mResult = -1; private int mPendingCount; @SuppressWarnings("unchecked") MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) { mListener = listener; mPendingCount = sets.length; mFutures = new Future[sets.length]; synchronized (this) { for (int i = 0, n = sets.length; i < n; ++i) { mFutures[i] = sets[i].requestSync(this); Log.d(TAG, " request sync: " + Utils.maskDebugInfo(sets[i].getName())); } } } @Override public synchronized void cancel() { if (mIsCancelled) return; mIsCancelled = true; for (Future future : mFutures) future.cancel(); if (mResult < 0) mResult = SYNC_RESULT_CANCELLED; } @Override public synchronized boolean isCancelled() { return mIsCancelled; } @Override public synchronized boolean isDone() { return mPendingCount == 0; } @Override public synchronized Integer get() { waitDone(); return mResult; } @Override public synchronized void waitDone() { try { while (!isDone()) wait(); } catch (InterruptedException e) { Log.d(TAG, "waitDone() interrupted"); } } // SyncListener callback @Override public void onSyncDone(MediaSet mediaSet, int resultCode) { SyncListener listener = null; synchronized (this) { if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR; --mPendingCount; if (mPendingCount == 0) { listener = mListener; notifyAll(); } Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " #pending=" + mPendingCount); } if (listener != null) listener.onSyncDone(MediaSet.this, mResult); } } }