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