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         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