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      */
onObjectIndexed(IngestObjectInfo object, int numVisited)54     public void onObjectIndexed(IngestObjectInfo object, int numVisited);
55 
56     /**
57      * The metadata loaded from the device is being sorted.
58      */
onSortingStarted()59     public void onSortingStarted();
60 
61     /**
62      * The indexing is done and the index is ready to be used.
63      */
onIndexingFinished()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 
getInstance()107   public static MtpDeviceIndex getInstance() {
108     return sInstance;
109   }
110 
MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory)111   protected MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory) {
112     mIndexRunnableFactory = indexRunnableFactory;
113   }
114 
getDevice()115   public synchronized MtpDevice getDevice() {
116     return mDevice;
117   }
118 
isDeviceConnected()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    */
isFormatSupported(int format)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    */
setDevice(MtpDevice device)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    */
getIndexRunnable()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    */
isIndexReady()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    */
setProgressListener(ProgressListener listener)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    */
unsetProgressListener(ProgressListener listener)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    */
size()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    */
get(int position, SortOrder order)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    */
getWithoutLabels(int position, SortOrder order)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    */
getPositionFromPositionWithoutLabels(int position, SortOrder order)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    */
getPositionWithoutLabelsFromPosition(int position, SortOrder order)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    */
sizeWithoutLabels()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    */
getFirstPositionForBucketNumber(int bucketNumber, SortOrder order)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    */
getBucketNumberForPosition(int position, SortOrder order)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    */
isFirstInBucket(int position, SortOrder order)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    */
getBuckets(SortOrder order)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 
resetState()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    */
isAtGeneration(MtpDevice device, long generation)395   protected boolean isAtGeneration(MtpDevice device, long generation) {
396     return (mGeneration == generation) && (mDevice == device);
397   }
398 
setIndexingResults(MtpDevice device, long generation, MtpDeviceIndexRunnable.Results results)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 
onIndexFinish(boolean successful)409   protected synchronized void onIndexFinish(boolean successful) {
410     if (!successful) {
411       resetState();
412     }
413     if (mProgressListener != null) {
414       mProgressListener.onIndexingFinished();
415     }
416   }
417 
onSorting()418   protected synchronized void onSorting() {
419     if (mProgressListener != null) {
420       mProgressListener.onSortingStarted();
421     }
422   }
423 
onObjectIndexed(IngestObjectInfo object, int numVisited)424   protected synchronized void onObjectIndexed(IngestObjectInfo object, int numVisited) {
425     if (mProgressListener != null) {
426       mProgressListener.onObjectIndexed(object, numVisited);
427     }
428   }
429 
getGeneration()430   protected long getGeneration() {
431     return mGeneration;
432   }
433 }
434