1package com.android.gallery3d.ingest.data;
2
3import android.annotation.TargetApi;
4import android.mtp.MtpConstants;
5import android.mtp.MtpDevice;
6import android.os.Build;
7
8import java.util.Collections;
9import java.util.HashSet;
10import 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)
43public 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