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