Home | History | Annotate | Download | only in data
      1 package com.android.gallery3d.ingest.data;
      2 
      3 import android.annotation.TargetApi;
      4 import android.mtp.MtpConstants;
      5 import android.mtp.MtpDevice;
      6 import android.os.Build;
      7 
      8 import java.util.Collections;
      9 import java.util.HashSet;
     10 import java.util.Set;
     11 
     12 /**
     13  * Index of MTP media objects organized into "buckets," or groupings, based on the date
     14  * they were created.
     15  *
     16  * When the index is created, the buckets are sorted in their natural
     17  * order, and the items within the buckets sorted by the date they are taken.
     18  *
     19  * The index enables the access of items and bucket labels as one unified list.
     20  * For example, let's say we have the following data in the index:
     21  *    [Bucket A]: [photo 1], [photo 2]
     22  *    [Bucket B]: [photo 3]
     23  *
     24  * Then the items can be thought of as being organized as a 5 element list:
     25  *   [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3]
     26  *
     27  * The data can also be accessed in descending order, in which case the list
     28  * would be a bit different from simply reversing the ascending list, since the
     29  * bucket labels need to always be at the beginning:
     30  *   [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1]
     31  *
     32  * The index enables all the following operations in constant time, both for
     33  * ascending and descending views of the data:
     34  *   - get/getAscending/getDescending: get an item at a specified list position
     35  *   - size: get the total number of items (bucket labels and MTP objects)
     36  *   - getFirstPositionForBucketNumber
     37  *   - getBucketNumberForPosition
     38  *   - isFirstInBucket
     39  *
     40  * See {@link MtpDeviceIndexRunnable} for implementation notes.
     41  */
     42 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
     43 public class MtpDeviceIndex {
     44 
     45   /**
     46    * Indexing progress listener.
     47    */
     48   public interface ProgressListener {
     49     /**
     50      * A media item on the device was indexed.
     51      * @param object The media item that was just indexed
     52      * @param numVisited Number of items visited so far
     53      */
     54     public void onObjectIndexed(IngestObjectInfo object, int numVisited);
     55 
     56     /**
     57      * The metadata loaded from the device is being sorted.
     58      */
     59     public void onSortingStarted();
     60 
     61     /**
     62      * The indexing is done and the index is ready to be used.
     63      */
     64     public void onIndexingFinished();
     65   }
     66 
     67   /**
     68    * Media sort orders.
     69    */
     70   public enum SortOrder {
     71     ASCENDING, DESCENDING
     72   }
     73 
     74   /** Quicktime MOV container (not already defined in {@link MtpConstants}) **/
     75   public static final int FORMAT_MOV = 0x300D;
     76 
     77   public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
     78   public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
     79 
     80   static {
     81     Set<Integer> supportedImageFormats = new HashSet<Integer>();
     82     supportedImageFormats.add(MtpConstants.FORMAT_JFIF);
     83     supportedImageFormats.add(MtpConstants.FORMAT_EXIF_JPEG);
     84     supportedImageFormats.add(MtpConstants.FORMAT_PNG);
     85     supportedImageFormats.add(MtpConstants.FORMAT_GIF);
     86     supportedImageFormats.add(MtpConstants.FORMAT_BMP);
     87     SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableSet(supportedImageFormats);
     88 
     89     Set<Integer> supportedVideoFormats = new HashSet<Integer>();
     90     supportedVideoFormats.add(MtpConstants.FORMAT_3GP_CONTAINER);
     91     supportedVideoFormats.add(MtpConstants.FORMAT_AVI);
     92     supportedVideoFormats.add(MtpConstants.FORMAT_MP4_CONTAINER);
     93     supportedVideoFormats.add(MtpConstants.FORMAT_MPEG);
     94     // TODO(georgescu): add FORMAT_MOV once Android Media Scanner supports .mov files
     95     SUPPORTED_VIDEO_FORMATS = Collections.unmodifiableSet(supportedVideoFormats);
     96   }
     97 
     98   private MtpDevice mDevice;
     99   private long mGeneration;
    100   private ProgressListener mProgressListener;
    101   private volatile MtpDeviceIndexRunnable.Results mResults;
    102   private final MtpDeviceIndexRunnable.Factory mIndexRunnableFactory;
    103 
    104   private static final MtpDeviceIndex sInstance = new MtpDeviceIndex(
    105       MtpDeviceIndexRunnable.getFactory());
    106 
    107   public static MtpDeviceIndex getInstance() {
    108     return sInstance;
    109   }
    110 
    111   protected MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory) {
    112     mIndexRunnableFactory = indexRunnableFactory;
    113   }
    114 
    115   public synchronized MtpDevice getDevice() {
    116     return mDevice;
    117   }
    118 
    119   public synchronized boolean isDeviceConnected() {
    120     return (mDevice != null);
    121   }
    122 
    123   /**
    124    * @param format Media format from {@link MtpConstants}
    125    * @return Whether the format is supported by this index.
    126    */
    127   public boolean isFormatSupported(int format) {
    128     return SUPPORTED_IMAGE_FORMATS.contains(format)
    129         || SUPPORTED_VIDEO_FORMATS.contains(format);
    130   }
    131 
    132   /**
    133    * Sets the MtpDevice that should be indexed and initializes state, but does
    134    * not kick off the actual indexing task, which is instead done by using
    135    * {@link #getIndexRunnable()}
    136    *
    137    * @param device The MtpDevice that should be indexed
    138    */
    139   public synchronized void setDevice(MtpDevice device) {
    140     if (device == mDevice) {
    141       return;
    142     }
    143     mDevice = device;
    144     resetState();
    145   }
    146 
    147   /**
    148    * Provides a Runnable for the indexing task (assuming the state has already
    149    * been correctly initialized by calling {@link #setDevice(MtpDevice)}).
    150    *
    151    * @return Runnable for the main indexing task
    152    */
    153   public synchronized Runnable getIndexRunnable() {
    154     if (!isDeviceConnected() || mResults != null) {
    155       return null;
    156     }
    157     return mIndexRunnableFactory.createMtpDeviceIndexRunnable(this);
    158   }
    159 
    160   /**
    161    * @return Whether the index is ready to be used.
    162    */
    163   public synchronized boolean isIndexReady() {
    164     return mResults != null;
    165   }
    166 
    167   /**
    168    * @param listener
    169    * @return Current progress (useful for configuring initial UI state)
    170    */
    171   public synchronized void setProgressListener(ProgressListener listener) {
    172     mProgressListener = listener;
    173   }
    174 
    175   /**
    176    * Make the listener null if it matches the argument
    177    *
    178    * @param listener Listener to unset, if currently registered
    179    */
    180   public synchronized void unsetProgressListener(ProgressListener listener) {
    181     if (mProgressListener == listener) {
    182       mProgressListener = null;
    183     }
    184   }
    185 
    186   /**
    187    * @return The total number of elements in the index (labels and items)
    188    */
    189   public int size() {
    190     MtpDeviceIndexRunnable.Results results = mResults;
    191     return results != null ? results.unifiedLookupIndex.length : 0;
    192   }
    193 
    194   /**
    195    * @param position Index of item to fetch, where 0 is the first item in the
    196    *            specified order
    197    * @param order
    198    * @return the bucket label or IngestObjectInfo at the specified position and
    199    *         order
    200    */
    201   public Object get(int position, SortOrder order) {
    202     MtpDeviceIndexRunnable.Results results = mResults;
    203     if (results == null) {
    204       return null;
    205     }
    206     if (order == SortOrder.ASCENDING) {
    207       DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]];
    208       if (bucket.unifiedStartIndex == position) {
    209         return bucket.date;
    210       } else {
    211         return results.mtpObjects[bucket.itemsStartIndex + position - 1
    212             - bucket.unifiedStartIndex];
    213       }
    214     } else {
    215       int zeroIndex = results.unifiedLookupIndex.length - 1 - position;
    216       DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]];
    217       if (bucket.unifiedEndIndex == zeroIndex) {
    218         return bucket.date;
    219       } else {
    220         return results.mtpObjects[bucket.itemsStartIndex + zeroIndex
    221             - bucket.unifiedStartIndex];
    222       }
    223     }
    224   }
    225 
    226   /**
    227    * @param position Index of item to fetch from a view of the data that does not
    228    *            include labels and is in the specified order
    229    * @return position-th item in specified order, when not including labels
    230    */
    231   public IngestObjectInfo getWithoutLabels(int position, SortOrder order) {
    232     MtpDeviceIndexRunnable.Results results = mResults;
    233     if (results == null) {
    234       return null;
    235     }
    236     if (order == SortOrder.ASCENDING) {
    237       return results.mtpObjects[position];
    238     } else {
    239       return results.mtpObjects[results.mtpObjects.length - 1 - position];
    240     }
    241   }
    242 
    243   /**
    244    * @param position Index of item to map from a view of the data that does not
    245    *            include labels and is in the specified order
    246    * @param order
    247    * @return position in a view of the data that does include labels, or -1 if the index isn't
    248    *         ready
    249    */
    250   public int getPositionFromPositionWithoutLabels(int position, SortOrder order) {
    251         /* Although this is O(log(number of buckets)), and thus should not be used
    252            in hotspots, even if the attached device has items for every day for
    253            a five-year timeframe, it would still only take 11 iterations at most,
    254            so shouldn't be a huge issue. */
    255     MtpDeviceIndexRunnable.Results results = mResults;
    256     if (results == null) {
    257       return -1;
    258     }
    259     if (order == SortOrder.DESCENDING) {
    260       position = results.mtpObjects.length - 1 - position;
    261     }
    262     int bucketNumber = 0;
    263     int iMin = 0;
    264     int iMax = results.buckets.length - 1;
    265     while (iMax >= iMin) {
    266       int iMid = (iMax + iMin) / 2;
    267       if (results.buckets[iMid].itemsStartIndex + results.buckets[iMid].numItems
    268           <= position) {
    269         iMin = iMid + 1;
    270       } else if (results.buckets[iMid].itemsStartIndex > position) {
    271         iMax = iMid - 1;
    272       } else {
    273         bucketNumber = iMid;
    274         break;
    275       }
    276     }
    277     int mappedPos = results.buckets[bucketNumber].unifiedStartIndex + position
    278         - results.buckets[bucketNumber].itemsStartIndex + 1;
    279     if (order == SortOrder.DESCENDING) {
    280       mappedPos = results.unifiedLookupIndex.length - mappedPos;
    281     }
    282     return mappedPos;
    283   }
    284 
    285   /**
    286    * @param position Index of item to map from a view of the data that
    287    *            includes labels and is in the specified order
    288    * @param order
    289    * @return position in a view of the data that does not include labels, or -1 if the index isn't
    290    *         ready
    291    */
    292   public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
    293     MtpDeviceIndexRunnable.Results results = mResults;
    294     if (results == null) {
    295       return -1;
    296     }
    297     if (order == SortOrder.ASCENDING) {
    298       DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]];
    299       if (bucket.unifiedStartIndex == position) {
    300         position++;
    301       }
    302       return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
    303     } else {
    304       int zeroIndex = results.unifiedLookupIndex.length - 1 - position;
    305       DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]];
    306       if (bucket.unifiedEndIndex == zeroIndex) {
    307         zeroIndex--;
    308       }
    309       return results.mtpObjects.length - 1 - bucket.itemsStartIndex
    310           - zeroIndex + bucket.unifiedStartIndex;
    311     }
    312   }
    313 
    314   /**
    315    * @return The number of media items in the index
    316    */
    317   public int sizeWithoutLabels() {
    318     MtpDeviceIndexRunnable.Results results = mResults;
    319     return results != null ? results.mtpObjects.length : 0;
    320   }
    321 
    322   /**
    323    * @param bucketNumber Index of bucket in the specified order
    324    * @param order
    325    * @return position of bucket's first item in a view of the data that includes labels
    326    */
    327   public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
    328     MtpDeviceIndexRunnable.Results results = mResults;
    329     if (order == SortOrder.ASCENDING) {
    330       return results.buckets[bucketNumber].unifiedStartIndex;
    331     } else {
    332       return results.unifiedLookupIndex.length
    333           - results.buckets[results.buckets.length - 1 - bucketNumber].unifiedEndIndex
    334           - 1;
    335     }
    336   }
    337 
    338   /**
    339    * @param position Index of item in the view of the data that includes labels and is in
    340    *                 the specified order
    341    * @param order
    342    * @return Index of the bucket that contains the specified item
    343    */
    344   public int getBucketNumberForPosition(int position, SortOrder order) {
    345     MtpDeviceIndexRunnable.Results results = mResults;
    346     if (order == SortOrder.ASCENDING) {
    347       return results.unifiedLookupIndex[position];
    348     } else {
    349       return results.buckets.length - 1
    350           - results.unifiedLookupIndex[results.unifiedLookupIndex.length - 1
    351           - position];
    352     }
    353   }
    354 
    355   /**
    356    * @param position Index of item in the view of the data that includes labels and is in
    357    *                 the specified order
    358    * @param order
    359    * @return Whether the specified item is the first item in its bucket
    360    */
    361   public boolean isFirstInBucket(int position, SortOrder order) {
    362     MtpDeviceIndexRunnable.Results results = mResults;
    363     if (order == SortOrder.ASCENDING) {
    364       return results.buckets[results.unifiedLookupIndex[position]].unifiedStartIndex
    365           == position;
    366     } else {
    367       position = results.unifiedLookupIndex.length - 1 - position;
    368       return results.buckets[results.unifiedLookupIndex[position]].unifiedEndIndex
    369           == position;
    370     }
    371   }
    372 
    373   /**
    374    * @param order
    375    * @return Array of buckets in the specified order
    376    */
    377   public DateBucket[] getBuckets(SortOrder order) {
    378     MtpDeviceIndexRunnable.Results results = mResults;
    379     if (results == null) {
    380       return null;
    381     }
    382     return (order == SortOrder.ASCENDING) ? results.buckets : results.reversedBuckets;
    383   }
    384 
    385   protected void resetState() {
    386     mGeneration++;
    387     mResults = null;
    388   }
    389 
    390   /**
    391    * @param device
    392    * @param generation
    393    * @return whether the index is at the given generation and the given device is connected
    394    */
    395   protected boolean isAtGeneration(MtpDevice device, long generation) {
    396     return (mGeneration == generation) && (mDevice == device);
    397   }
    398 
    399   protected synchronized boolean setIndexingResults(MtpDevice device, long generation,
    400       MtpDeviceIndexRunnable.Results results) {
    401     if (!isAtGeneration(device, generation)) {
    402       return false;
    403     }
    404     mResults = results;
    405     onIndexFinish(true /*successful*/);
    406     return true;
    407   }
    408 
    409   protected synchronized void onIndexFinish(boolean successful) {
    410     if (!successful) {
    411       resetState();
    412     }
    413     if (mProgressListener != null) {
    414       mProgressListener.onIndexingFinished();
    415     }
    416   }
    417 
    418   protected synchronized void onSorting() {
    419     if (mProgressListener != null) {
    420       mProgressListener.onSortingStarted();
    421     }
    422   }
    423 
    424   protected synchronized void onObjectIndexed(IngestObjectInfo object, int numVisited) {
    425     if (mProgressListener != null) {
    426       mProgressListener.onObjectIndexed(object, numVisited);
    427     }
    428   }
    429 
    430   protected long getGeneration() {
    431     return mGeneration;
    432   }
    433 }
    434