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