Home | History | Annotate | Download | only in data
      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