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      */
onObjectIndexed(IngestObjectInfo object, int numVisited)59     public void onObjectIndexed(IngestObjectInfo object, int numVisited);
60 
61     /**
62      * The metadata loaded from the device is being sorted.
63      */
onSortingStarted()64     public void onSortingStarted();
65 
66     /**
67      * The indexing is done and the index is ready to be used.
68      */
onIndexingFinished()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 
getInstance()120   public static MtpDeviceIndex getInstance() {
121     return sInstance;
122   }
123 
MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory)124   protected MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory) {
125     mIndexRunnableFactory = indexRunnableFactory;
126   }
127 
getDevice()128   public synchronized MtpDevice getDevice() {
129     return mDevice;
130   }
131 
isDeviceConnected()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    */
isFormatSupported(MtpObjectInfo mtpObjectInfo)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    */
setDevice(MtpDevice device)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    */
getIndexRunnable()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    */
isIndexReady()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    */
setProgressListener(ProgressListener listener)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    */
unsetProgressListener(ProgressListener listener)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    */
size()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    */
get(int position, SortOrder order)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    */
getWithoutLabels(int position, SortOrder order)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    */
getPositionFromPositionWithoutLabels(int position, SortOrder order)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    */
getPositionWithoutLabelsFromPosition(int position, SortOrder order)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    */
sizeWithoutLabels()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    */
getFirstPositionForBucketNumber(int bucketNumber, SortOrder order)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    */
getBucketNumberForPosition(int position, SortOrder order)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    */
isFirstInBucket(int position, SortOrder order)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    */
getBuckets(SortOrder order)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 
resetState()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    */
isAtGeneration(MtpDevice device, long generation)437   protected boolean isAtGeneration(MtpDevice device, long generation) {
438     return (mGeneration == generation) && (mDevice == device);
439   }
440 
setIndexingResults(MtpDevice device, long generation, MtpDeviceIndexRunnable.Results results)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 
onIndexFinish(boolean successful)451   protected synchronized void onIndexFinish(boolean successful) {
452     if (!successful) {
453       resetState();
454     }
455     if (mProgressListener != null) {
456       mProgressListener.onIndexingFinished();
457     }
458   }
459 
onSorting()460   protected synchronized void onSorting() {
461     if (mProgressListener != null) {
462       mProgressListener.onSortingStarted();
463     }
464   }
465 
onObjectIndexed(IngestObjectInfo object, int numVisited)466   protected synchronized void onObjectIndexed(IngestObjectInfo object, int numVisited) {
467     if (mProgressListener != null) {
468       mProgressListener.onObjectIndexed(object, numVisited);
469     }
470   }
471 
getGeneration()472   protected long getGeneration() {
473     return mGeneration;
474   }
475 }
476