1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.gallery3d.data; 18 19 import com.android.gallery3d.common.Utils; 20 import com.android.gallery3d.util.Future; 21 22 import java.util.ArrayList; 23 import java.util.WeakHashMap; 24 25 // MediaSet is a directory-like data structure. 26 // It contains MediaItems and sub-MediaSets. 27 // 28 // The primary interface are: 29 // getMediaItemCount(), getMediaItem() and 30 // getSubMediaSetCount(), getSubMediaSet(). 31 // 32 // getTotalMediaItemCount() returns the number of all MediaItems, including 33 // those in sub-MediaSets. 34 public abstract class MediaSet extends MediaObject { 35 private static final String TAG = "MediaSet"; 36 37 public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500; 38 public static final int INDEX_NOT_FOUND = -1; 39 40 public static final int SYNC_RESULT_SUCCESS = 0; 41 public static final int SYNC_RESULT_CANCELLED = 1; 42 public static final int SYNC_RESULT_ERROR = 2; 43 44 /** Listener to be used with requestSync(SyncListener). */ 45 public static interface SyncListener { 46 /** 47 * Called when the sync task completed. Completion may be due to normal termination, 48 * an exception, or cancellation. 49 * 50 * @param mediaSet the MediaSet that's done with sync 51 * @param resultCode one of the SYNC_RESULT_* constants 52 */ 53 void onSyncDone(MediaSet mediaSet, int resultCode); 54 } 55 56 public MediaSet(Path path, long version) { 57 super(path, version); 58 } 59 60 public int getMediaItemCount() { 61 return 0; 62 } 63 64 // Returns the media items in the range [start, start + count). 65 // 66 // The number of media items returned may be less than the specified count 67 // if there are not enough media items available. The number of 68 // media items available may not be consistent with the return value of 69 // getMediaItemCount() because the contents of database may have already 70 // changed. 71 public ArrayList<MediaItem> getMediaItem(int start, int count) { 72 return new ArrayList<MediaItem>(); 73 } 74 75 public MediaItem getCoverMediaItem() { 76 ArrayList<MediaItem> items = getMediaItem(0, 1); 77 if (items.size() > 0) return items.get(0); 78 for (int i = 0, n = getSubMediaSetCount(); i < n; i++) { 79 MediaItem cover = getSubMediaSet(i).getCoverMediaItem(); 80 if (cover != null) return cover; 81 } 82 return null; 83 } 84 85 public int getSubMediaSetCount() { 86 return 0; 87 } 88 89 public MediaSet getSubMediaSet(int index) { 90 throw new IndexOutOfBoundsException(); 91 } 92 93 public boolean isLeafAlbum() { 94 return false; 95 } 96 97 public int getTotalMediaItemCount() { 98 int total = getMediaItemCount(); 99 for (int i = 0, n = getSubMediaSetCount(); i < n; i++) { 100 total += getSubMediaSet(i).getTotalMediaItemCount(); 101 } 102 return total; 103 } 104 105 // TODO: we should have better implementation of sub classes 106 public int getIndexOfItem(Path path, int hint) { 107 // hint < 0 is handled below 108 // first, try to find it around the hint 109 int start = Math.max(0, 110 hint - MEDIAITEM_BATCH_FETCH_COUNT / 2); 111 ArrayList<MediaItem> list = getMediaItem( 112 start, MEDIAITEM_BATCH_FETCH_COUNT); 113 int index = getIndexOf(path, list); 114 if (index != INDEX_NOT_FOUND) return start + index; 115 116 // try to find it globally 117 start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0; 118 list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); 119 while (true) { 120 index = getIndexOf(path, list); 121 if (index != INDEX_NOT_FOUND) return start + index; 122 if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND; 123 start += MEDIAITEM_BATCH_FETCH_COUNT; 124 list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT); 125 } 126 } 127 128 protected int getIndexOf(Path path, ArrayList<MediaItem> list) { 129 for (int i = 0, n = list.size(); i < n; ++i) { 130 if (list.get(i).mPath == path) return i; 131 } 132 return INDEX_NOT_FOUND; 133 } 134 135 public abstract String getName(); 136 137 private WeakHashMap<ContentListener, Object> mListeners = 138 new WeakHashMap<ContentListener, Object>(); 139 140 // NOTE: The MediaSet only keeps a weak reference to the listener. The 141 // listener is automatically removed when there is no other reference to 142 // the listener. 143 public void addContentListener(ContentListener listener) { 144 if (mListeners.containsKey(listener)) { 145 throw new IllegalArgumentException(); 146 } 147 mListeners.put(listener, null); 148 } 149 150 public void removeContentListener(ContentListener listener) { 151 if (!mListeners.containsKey(listener)) { 152 throw new IllegalArgumentException(); 153 } 154 mListeners.remove(listener); 155 } 156 157 // This should be called by subclasses when the content is changed. 158 public void notifyContentChanged() { 159 for (ContentListener listener : mListeners.keySet()) { 160 listener.onContentDirty(); 161 } 162 } 163 164 // Reload the content. Return the current data version. reload() should be called 165 // in the same thread as getMediaItem(int, int) and getSubMediaSet(int). 166 public abstract long reload(); 167 168 @Override 169 public MediaDetails getDetails() { 170 MediaDetails details = super.getDetails(); 171 details.addDetail(MediaDetails.INDEX_TITLE, getName()); 172 return details; 173 } 174 175 // Enumerate all media items in this media set (including the ones in sub 176 // media sets), in an efficient order. ItemConsumer.consumer() will be 177 // called for each media item with its index. 178 public void enumerateMediaItems(ItemConsumer consumer) { 179 enumerateMediaItems(consumer, 0); 180 } 181 182 public void enumerateTotalMediaItems(ItemConsumer consumer) { 183 enumerateTotalMediaItems(consumer, 0); 184 } 185 186 public static interface ItemConsumer { 187 void consume(int index, MediaItem item); 188 } 189 190 // The default implementation uses getMediaItem() for enumerateMediaItems(). 191 // Subclasses may override this and use more efficient implementations. 192 // Returns the number of items enumerated. 193 protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { 194 int total = getMediaItemCount(); 195 int start = 0; 196 while (start < total) { 197 int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start); 198 ArrayList<MediaItem> items = getMediaItem(start, count); 199 for (int i = 0, n = items.size(); i < n; i++) { 200 MediaItem item = items.get(i); 201 consumer.consume(startIndex + start + i, item); 202 } 203 start += count; 204 } 205 return total; 206 } 207 208 // Recursively enumerate all media items under this set. 209 // Returns the number of items enumerated. 210 protected int enumerateTotalMediaItems( 211 ItemConsumer consumer, int startIndex) { 212 int start = 0; 213 start += enumerateMediaItems(consumer, startIndex); 214 int m = getSubMediaSetCount(); 215 for (int i = 0; i < m; i++) { 216 start += getSubMediaSet(i).enumerateTotalMediaItems( 217 consumer, startIndex + start); 218 } 219 return start; 220 } 221 222 /** 223 * Requests sync on this MediaSet. It returns a Future object that can be used by the caller 224 * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants 225 * defined in this class and can be obtained by Future.get(). 226 * 227 * Subclasses should perform sync on a different thread. 228 * 229 * The default implementation here returns a Future stub that does nothing and returns 230 * SYNC_RESULT_SUCCESS by get(). 231 */ 232 public Future<Integer> requestSync(SyncListener listener) { 233 listener.onSyncDone(this, SYNC_RESULT_SUCCESS); 234 return FUTURE_STUB; 235 } 236 237 private static final Future<Integer> FUTURE_STUB = new Future<Integer>() { 238 @Override 239 public void cancel() {} 240 241 @Override 242 public boolean isCancelled() { 243 return false; 244 } 245 246 @Override 247 public boolean isDone() { 248 return true; 249 } 250 251 @Override 252 public Integer get() { 253 return SYNC_RESULT_SUCCESS; 254 } 255 256 @Override 257 public void waitDone() {} 258 }; 259 260 protected Future<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) { 261 return new MultiSetSyncFuture(sets, listener); 262 } 263 264 private class MultiSetSyncFuture implements Future<Integer>, SyncListener { 265 private static final String TAG = "Gallery.MultiSetSync"; 266 267 private final SyncListener mListener; 268 private final Future<Integer> mFutures[]; 269 270 private boolean mIsCancelled = false; 271 private int mResult = -1; 272 private int mPendingCount; 273 274 @SuppressWarnings("unchecked") 275 MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) { 276 mListener = listener; 277 mPendingCount = sets.length; 278 mFutures = new Future[sets.length]; 279 280 synchronized (this) { 281 for (int i = 0, n = sets.length; i < n; ++i) { 282 mFutures[i] = sets[i].requestSync(this); 283 Log.d(TAG, " request sync: " + Utils.maskDebugInfo(sets[i].getName())); 284 } 285 } 286 } 287 288 @Override 289 public synchronized void cancel() { 290 if (mIsCancelled) return; 291 mIsCancelled = true; 292 for (Future<Integer> future : mFutures) future.cancel(); 293 if (mResult < 0) mResult = SYNC_RESULT_CANCELLED; 294 } 295 296 @Override 297 public synchronized boolean isCancelled() { 298 return mIsCancelled; 299 } 300 301 @Override 302 public synchronized boolean isDone() { 303 return mPendingCount == 0; 304 } 305 306 @Override 307 public synchronized Integer get() { 308 waitDone(); 309 return mResult; 310 } 311 312 @Override 313 public synchronized void waitDone() { 314 try { 315 while (!isDone()) wait(); 316 } catch (InterruptedException e) { 317 Log.d(TAG, "waitDone() interrupted"); 318 } 319 } 320 321 // SyncListener callback 322 @Override 323 public void onSyncDone(MediaSet mediaSet, int resultCode) { 324 SyncListener listener = null; 325 synchronized (this) { 326 if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR; 327 --mPendingCount; 328 if (mPendingCount == 0) { 329 listener = mListener; 330 notifyAll(); 331 } 332 Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) 333 + " #pending=" + mPendingCount); 334 } 335 if (listener != null) listener.onSyncDone(MediaSet.this, mResult); 336 } 337 } 338 } 339