blob: 87b5f56efc8ac44ca75a64d3b6468675df614fe1 [file] [log] [blame]
/*
* 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<MediaItem> getMediaItem(int start, int count) {
return new ArrayList<MediaItem>();
}
public MediaItem getCoverMediaItem() {
ArrayList<MediaItem> 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;
}
public boolean isCameraRoll() {
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<MediaItem> 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<MediaItem> 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<ContentListener, Object> mListeners =
new WeakHashMap<ContentListener, Object>();
// 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<MediaItem> 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<Integer> requestSync(SyncListener listener) {
listener.onSyncDone(this, SYNC_RESULT_SUCCESS);
return FUTURE_STUB;
}
private static final Future<Integer> FUTURE_STUB = new Future<Integer>() {
@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<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) {
return new MultiSetSyncFuture(sets, listener);
}
private class MultiSetSyncFuture implements Future<Integer>, SyncListener {
@SuppressWarnings("hiding")
private static final String TAG = "Gallery.MultiSetSync";
private final SyncListener mListener;
private final Future<Integer> 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<Integer> 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);
}
}
}