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