1 package com.android.gallery3d.ingest.data;
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;
10 import java.util.Collections;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.Locale;
18 * Index of MTP media objects organized into "buckets," or groupings, based on the date
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.
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]
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]
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]
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
45 * See {@link MtpDeviceIndexRunnable} for implementation notes.
47 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
48 public class MtpDeviceIndex {
51 * Indexing progress listener.
53 public interface ProgressListener {
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
59 public void onObjectIndexed(IngestObjectInfo object, int numVisited);
62 * The metadata loaded from the device is being sorted.
64 public void onSortingStarted();
67 * The indexing is done and the index is ready to be used.
69 public void onIndexingFinished();
75 public enum SortOrder {
79 /** Quicktime MOV container (not already defined in {@link MtpConstants}) **/
80 public static final int FORMAT_MOV = 0x300D;
82 public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
83 public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
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);
97 SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableSet(supportedImageFormats);
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);
109 private MtpDevice mDevice;
110 private long mGeneration;
111 private ProgressListener mProgressListener;
112 private volatile MtpDeviceIndexRunnable.Results mResults;
113 private final MtpDeviceIndexRunnable.Factory mIndexRunnableFactory;
115 private static final MtpDeviceIndex sInstance = new MtpDeviceIndex(
116 MtpDeviceIndexRunnable.getFactory());
118 private static final Map<String, Boolean> sCachedSupportedExtenstions = new HashMap<>();
120 public static MtpDeviceIndex getInstance() {
124 protected MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory) {
125 mIndexRunnableFactory = indexRunnableFactory;
128 public synchronized MtpDevice getDevice() {
132 public synchronized boolean isDeviceConnected() {
133 return (mDevice != null);
137 * @param mtpObjectInfo MTP object info
138 * @return Whether the format is supported by this index.
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)) {
148 // Checks whether the extension is supported or not.
149 final String name = mtpObjectInfo.getName();
153 final int lastDot = name.lastIndexOf('.');
155 final String extension = name.substring(lastDot + 1);
157 Boolean result = sCachedSupportedExtenstions.get(extension);
158 if (result != null) {
161 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
162 extension.toLowerCase(Locale.US));
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);
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()}
179 * @param device The MtpDevice that should be indexed
181 public synchronized void setDevice(MtpDevice device) {
182 if (device == mDevice) {
190 * Provides a Runnable for the indexing task (assuming the state has already
191 * been correctly initialized by calling {@link #setDevice(MtpDevice)}).
193 * @return Runnable for the main indexing task
195 public synchronized Runnable getIndexRunnable() {
196 if (!isDeviceConnected() || mResults != null) {
199 return mIndexRunnableFactory.createMtpDeviceIndexRunnable(this);
203 * @return Whether the index is ready to be used.
205 public synchronized boolean isIndexReady() {
206 return mResults != null;
211 * @return Current progress (useful for configuring initial UI state)
213 public synchronized void setProgressListener(ProgressListener listener) {
214 mProgressListener = listener;
218 * Make the listener null if it matches the argument
220 * @param listener Listener to unset, if currently registered
222 public synchronized void unsetProgressListener(ProgressListener listener) {
223 if (mProgressListener == listener) {
224 mProgressListener = null;
229 * @return The total number of elements in the index (labels and items)
232 MtpDeviceIndexRunnable.Results results = mResults;
233 return results != null ? results.unifiedLookupIndex.length : 0;
237 * @param position Index of item to fetch, where 0 is the first item in the
240 * @return the bucket label or IngestObjectInfo at the specified position and
243 public Object get(int position, SortOrder order) {
244 MtpDeviceIndexRunnable.Results results = mResults;
245 if (results == null) {
248 if (order == SortOrder.ASCENDING) {
249 DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]];
250 if (bucket.unifiedStartIndex == position) {
253 return results.mtpObjects[bucket.itemsStartIndex + position - 1
254 - bucket.unifiedStartIndex];
257 int zeroIndex = results.unifiedLookupIndex.length - 1 - position;
258 DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]];
259 if (bucket.unifiedEndIndex == zeroIndex) {
262 return results.mtpObjects[bucket.itemsStartIndex + zeroIndex
263 - bucket.unifiedStartIndex];
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
273 public IngestObjectInfo getWithoutLabels(int position, SortOrder order) {
274 MtpDeviceIndexRunnable.Results results = mResults;
275 if (results == null) {
278 if (order == SortOrder.ASCENDING) {
279 return results.mtpObjects[position];
281 return results.mtpObjects[results.mtpObjects.length - 1 - position];
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
289 * @return position in a view of the data that does include labels, or -1 if the index isn't
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) {
301 if (order == SortOrder.DESCENDING) {
302 position = results.mtpObjects.length - 1 - position;
304 int bucketNumber = 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
312 } else if (results.buckets[iMid].itemsStartIndex > position) {
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;
328 * @param position Index of item to map from a view of the data that
329 * includes labels and is in the specified order
331 * @return position in a view of the data that does not include labels, or -1 if the index isn't
334 public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
335 MtpDeviceIndexRunnable.Results results = mResults;
336 if (results == null) {
339 if (order == SortOrder.ASCENDING) {
340 DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]];
341 if (bucket.unifiedStartIndex == position) {
344 return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
346 int zeroIndex = results.unifiedLookupIndex.length - 1 - position;
347 DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]];
348 if (bucket.unifiedEndIndex == zeroIndex) {
351 return results.mtpObjects.length - 1 - bucket.itemsStartIndex
352 - zeroIndex + bucket.unifiedStartIndex;
357 * @return The number of media items in the index
359 public int sizeWithoutLabels() {
360 MtpDeviceIndexRunnable.Results results = mResults;
361 return results != null ? results.mtpObjects.length : 0;
365 * @param bucketNumber Index of bucket in the specified order
367 * @return position of bucket's first item in a view of the data that includes labels
369 public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
370 MtpDeviceIndexRunnable.Results results = mResults;
371 if (order == SortOrder.ASCENDING) {
372 return results.buckets[bucketNumber].unifiedStartIndex;
374 return results.unifiedLookupIndex.length
375 - results.buckets[results.buckets.length - 1 - bucketNumber].unifiedEndIndex
381 * @param position Index of item in the view of the data that includes labels and is in
382 * the specified order
384 * @return Index of the bucket that contains the specified item
386 public int getBucketNumberForPosition(int position, SortOrder order) {
387 MtpDeviceIndexRunnable.Results results = mResults;
388 if (order == SortOrder.ASCENDING) {
389 return results.unifiedLookupIndex[position];
391 return results.buckets.length - 1
392 - results.unifiedLookupIndex[results.unifiedLookupIndex.length - 1
398 * @param position Index of item in the view of the data that includes labels and is in
399 * the specified order
401 * @return Whether the specified item is the first item in its bucket
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
409 position = results.unifiedLookupIndex.length - 1 - position;
410 return results.buckets[results.unifiedLookupIndex[position]].unifiedEndIndex
417 * @return Array of buckets in the specified order
419 public DateBucket[] getBuckets(SortOrder order) {
420 MtpDeviceIndexRunnable.Results results = mResults;
421 if (results == null) {
424 return (order == SortOrder.ASCENDING) ? results.buckets : results.reversedBuckets;
427 protected void resetState() {
435 * @return whether the index is at the given generation and the given device is connected
437 protected boolean isAtGeneration(MtpDevice device, long generation) {
438 return (mGeneration == generation) && (mDevice == device);
441 protected synchronized boolean setIndexingResults(MtpDevice device, long generation,
442 MtpDeviceIndexRunnable.Results results) {
443 if (!isAtGeneration(device, generation)) {
447 onIndexFinish(true /*successful*/);
451 protected synchronized void onIndexFinish(boolean successful) {
455 if (mProgressListener != null) {
456 mProgressListener.onIndexingFinished();
460 protected synchronized void onSorting() {
461 if (mProgressListener != null) {
462 mProgressListener.onSortingStarted();
466 protected synchronized void onObjectIndexed(IngestObjectInfo object, int numVisited) {
467 if (mProgressListener != null) {
468 mProgressListener.onObjectIndexed(object, numVisited);
472 protected long getGeneration() {