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 mListeners.put(listener, null); 160 } 161 162 public void removeContentListener(ContentListener listener) { 163 mListeners.remove(listener); 164 } 165 166 // This should be called by subclasses when the content is changed. 167 public void notifyContentChanged() { 168 for (ContentListener listener : mListeners.keySet()) { 169 listener.onContentDirty(); 170 } 171 } 172 173 // Reload the content. Return the current data version. reload() should be called 174 // in the same thread as getMediaItem(int, int) and getSubMediaSet(int). 175 public abstract long reload(); 176 177 @Override 178 public MediaDetails getDetails() { 179 MediaDetails details = super.getDetails(); 180 details.addDetail(MediaDetails.INDEX_TITLE, getName()); 181 return details; 182 } 183 184 // Enumerate all media items in this media set (including the ones in sub 185 // media sets), in an efficient order. ItemConsumer.consumer() will be 186 // called for each media item with its index. 187 public void enumerateMediaItems(ItemConsumer consumer) { 188 enumerateMediaItems(consumer, 0); 189 } 190 191 public void enumerateTotalMediaItems(ItemConsumer consumer) { 192 enumerateTotalMediaItems(consumer, 0); 193 } 194 195 public static interface ItemConsumer { 196 void consume(int index, MediaItem item); 197 } 198 199 // The default implementation uses getMediaItem() for enumerateMediaItems(). 200 // Subclasses may override this and use more efficient implementations. 201 // Returns the number of items enumerated. 202 protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) { 203 int total = getMediaItemCount(); 204 int start = 0; 205 while (start < total) { 206 int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start); 207 ArrayList<MediaItem> items = getMediaItem(start, count); 208 for (int i = 0, n = items.size(); i < n; i++) { 209 MediaItem item = items.get(i); 210 consumer.consume(startIndex + start + i, item); 211 } 212 start += count; 213 } 214 return total; 215 } 216 217 // Recursively enumerate all media items under this set. 218 // Returns the number of items enumerated. 219 protected int enumerateTotalMediaItems( 220 ItemConsumer consumer, int startIndex) { 221 int start = 0; 222 start += enumerateMediaItems(consumer, startIndex); 223 int m = getSubMediaSetCount(); 224 for (int i = 0; i < m; i++) { 225 start += getSubMediaSet(i).enumerateTotalMediaItems( 226 consumer, startIndex + start); 227 } 228 return start; 229 } 230 231 /** 232 * Requests sync on this MediaSet. It returns a Future object that can be used by the caller 233 * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants 234 * defined in this class and can be obtained by Future.get(). 235 * 236 * Subclasses should perform sync on a different thread. 237 * 238 * The default implementation here returns a Future stub that does nothing and returns 239 * SYNC_RESULT_SUCCESS by get(). 240 */ 241 public Future<Integer> requestSync(SyncListener listener) { 242 listener.onSyncDone(this, SYNC_RESULT_SUCCESS); 243 return FUTURE_STUB; 244 } 245 246 private static final Future<Integer> FUTURE_STUB = new Future<Integer>() { 247 @Override 248 public void cancel() {} 249 250 @Override 251 public boolean isCancelled() { 252 return false; 253 } 254 255 @Override 256 public boolean isDone() { 257 return true; 258 } 259 260 @Override 261 public Integer get() { 262 return SYNC_RESULT_SUCCESS; 263 } 264 265 @Override 266 public void waitDone() {} 267 }; 268 269 protected Future<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) { 270 return new MultiSetSyncFuture(sets, listener); 271 } 272 273 private class MultiSetSyncFuture implements Future<Integer>, SyncListener { 274 @SuppressWarnings("hiding") 275 private static final String TAG = "Gallery.MultiSetSync"; 276 277 private final SyncListener mListener; 278 private final Future<Integer> mFutures[]; 279 280 private boolean mIsCancelled = false; 281 private int mResult = -1; 282 private int mPendingCount; 283 284 @SuppressWarnings("unchecked") 285 MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) { 286 mListener = listener; 287 mPendingCount = sets.length; 288 mFutures = new Future[sets.length]; 289 290 synchronized (this) { 291 for (int i = 0, n = sets.length; i < n; ++i) { 292 mFutures[i] = sets[i].requestSync(this); 293 Log.d(TAG, " request sync: " + Utils.maskDebugInfo(sets[i].getName())); 294 } 295 } 296 } 297 298 @Override 299 public synchronized void cancel() { 300 if (mIsCancelled) return; 301 mIsCancelled = true; 302 for (Future<Integer> future : mFutures) future.cancel(); 303 if (mResult < 0) mResult = SYNC_RESULT_CANCELLED; 304 } 305 306 @Override 307 public synchronized boolean isCancelled() { 308 return mIsCancelled; 309 } 310 311 @Override 312 public synchronized boolean isDone() { 313 return mPendingCount == 0; 314 } 315 316 @Override 317 public synchronized Integer get() { 318 waitDone(); 319 return mResult; 320 } 321 322 @Override 323 public synchronized void waitDone() { 324 try { 325 while (!isDone()) wait(); 326 } catch (InterruptedException e) { 327 Log.d(TAG, "waitDone() interrupted"); 328 } 329 } 330 331 // SyncListener callback 332 @Override 333 public void onSyncDone(MediaSet mediaSet, int resultCode) { 334 SyncListener listener = null; 335 synchronized (this) { 336 if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR; 337 --mPendingCount; 338 if (mPendingCount == 0) { 339 listener = mListener; 340 notifyAll(); 341 } 342 Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) 343 + " #pending=" + mPendingCount); 344 } 345 if (listener != null) listener.onSyncDone(MediaSet.this, mResult); 346 } 347 } 348 } 349