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