1package com.android.gallery3d.ingest.data;
2
3import android.annotation.TargetApi;
4import android.mtp.MtpConstants;
5import android.mtp.MtpDevice;
6import android.mtp.MtpObjectInfo;
7import android.os.Build;
8import android.webkit.MimeTypeMap;
9
10import java.util.Collections;
11import java.util.HashMap;
12import java.util.HashSet;
13import java.util.Locale;
14import java.util.Map;
15import 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)
48public 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