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.os.Build;
8 import java.util.Collections;
9 import java.util.HashSet;
13 * Index of MTP media objects organized into "buckets," or groupings, based on the date
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.
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]
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]
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]
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
40 * See {@link MtpDeviceIndexRunnable} for implementation notes.
42 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
43 public class MtpDeviceIndex {
46 * Indexing progress listener.
48 public interface ProgressListener {
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
54 public void onObjectIndexed(IngestObjectInfo object, int numVisited);
57 * The metadata loaded from the device is being sorted.
59 public void onSortingStarted();
62 * The indexing is done and the index is ready to be used.
64 public void onIndexingFinished();
70 public enum SortOrder {
74 /** Quicktime MOV container (not already defined in {@link MtpConstants}) **/
75 public static final int FORMAT_MOV = 0x300D;
77 public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
78 public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
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);
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);
98 private MtpDevice mDevice;
99 private long mGeneration;
100 private ProgressListener mProgressListener;
101 private volatile MtpDeviceIndexRunnable.Results mResults;
102 private final MtpDeviceIndexRunnable.Factory mIndexRunnableFactory;
104 private static final MtpDeviceIndex sInstance = new MtpDeviceIndex(
105 MtpDeviceIndexRunnable.getFactory());
107 public static MtpDeviceIndex getInstance() {
111 protected MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory) {
112 mIndexRunnableFactory = indexRunnableFactory;
115 public synchronized MtpDevice getDevice() {
119 public synchronized boolean isDeviceConnected() {
120 return (mDevice != null);
124 * @param format Media format from {@link MtpConstants}
125 * @return Whether the format is supported by this index.
127 public boolean isFormatSupported(int format) {
128 return SUPPORTED_IMAGE_FORMATS.contains(format)
129 || SUPPORTED_VIDEO_FORMATS.contains(format);
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()}
137 * @param device The MtpDevice that should be indexed
139 public synchronized void setDevice(MtpDevice device) {
140 if (device == mDevice) {
148 * Provides a Runnable for the indexing task (assuming the state has already
149 * been correctly initialized by calling {@link #setDevice(MtpDevice)}).
151 * @return Runnable for the main indexing task
153 public synchronized Runnable getIndexRunnable() {
154 if (!isDeviceConnected() || mResults != null) {
157 return mIndexRunnableFactory.createMtpDeviceIndexRunnable(this);
161 * @return Whether the index is ready to be used.
163 public synchronized boolean isIndexReady() {
164 return mResults != null;
169 * @return Current progress (useful for configuring initial UI state)
171 public synchronized void setProgressListener(ProgressListener listener) {
172 mProgressListener = listener;
176 * Make the listener null if it matches the argument
178 * @param listener Listener to unset, if currently registered
180 public synchronized void unsetProgressListener(ProgressListener listener) {
181 if (mProgressListener == listener) {
182 mProgressListener = null;
187 * @return The total number of elements in the index (labels and items)
190 MtpDeviceIndexRunnable.Results results = mResults;
191 return results != null ? results.unifiedLookupIndex.length : 0;
195 * @param position Index of item to fetch, where 0 is the first item in the
198 * @return the bucket label or IngestObjectInfo at the specified position and
201 public Object get(int position, SortOrder order) {
202 MtpDeviceIndexRunnable.Results results = mResults;
203 if (results == null) {
206 if (order == SortOrder.ASCENDING) {
207 DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]];
208 if (bucket.unifiedStartIndex == position) {
211 return results.mtpObjects[bucket.itemsStartIndex + position - 1
212 - bucket.unifiedStartIndex];
215 int zeroIndex = results.unifiedLookupIndex.length - 1 - position;
216 DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]];
217 if (bucket.unifiedEndIndex == zeroIndex) {
220 return results.mtpObjects[bucket.itemsStartIndex + zeroIndex
221 - bucket.unifiedStartIndex];
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
231 public IngestObjectInfo getWithoutLabels(int position, SortOrder order) {
232 MtpDeviceIndexRunnable.Results results = mResults;
233 if (results == null) {
236 if (order == SortOrder.ASCENDING) {
237 return results.mtpObjects[position];
239 return results.mtpObjects[results.mtpObjects.length - 1 - position];
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
247 * @return position in a view of the data that does include labels, or -1 if the index isn't
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) {
259 if (order == SortOrder.DESCENDING) {
260 position = results.mtpObjects.length - 1 - position;
262 int bucketNumber = 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
270 } else if (results.buckets[iMid].itemsStartIndex > position) {
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;
286 * @param position Index of item to map from a view of the data that
287 * includes labels and is in the specified order
289 * @return position in a view of the data that does not include labels, or -1 if the index isn't
292 public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
293 MtpDeviceIndexRunnable.Results results = mResults;
294 if (results == null) {
297 if (order == SortOrder.ASCENDING) {
298 DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]];
299 if (bucket.unifiedStartIndex == position) {
302 return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
304 int zeroIndex = results.unifiedLookupIndex.length - 1 - position;
305 DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]];
306 if (bucket.unifiedEndIndex == zeroIndex) {
309 return results.mtpObjects.length - 1 - bucket.itemsStartIndex
310 - zeroIndex + bucket.unifiedStartIndex;
315 * @return The number of media items in the index
317 public int sizeWithoutLabels() {
318 MtpDeviceIndexRunnable.Results results = mResults;
319 return results != null ? results.mtpObjects.length : 0;
323 * @param bucketNumber Index of bucket in the specified order
325 * @return position of bucket's first item in a view of the data that includes labels
327 public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
328 MtpDeviceIndexRunnable.Results results = mResults;
329 if (order == SortOrder.ASCENDING) {
330 return results.buckets[bucketNumber].unifiedStartIndex;
332 return results.unifiedLookupIndex.length
333 - results.buckets[results.buckets.length - 1 - bucketNumber].unifiedEndIndex
339 * @param position Index of item in the view of the data that includes labels and is in
340 * the specified order
342 * @return Index of the bucket that contains the specified item
344 public int getBucketNumberForPosition(int position, SortOrder order) {
345 MtpDeviceIndexRunnable.Results results = mResults;
346 if (order == SortOrder.ASCENDING) {
347 return results.unifiedLookupIndex[position];
349 return results.buckets.length - 1
350 - results.unifiedLookupIndex[results.unifiedLookupIndex.length - 1
356 * @param position Index of item in the view of the data that includes labels and is in
357 * the specified order
359 * @return Whether the specified item is the first item in its bucket
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
367 position = results.unifiedLookupIndex.length - 1 - position;
368 return results.buckets[results.unifiedLookupIndex[position]].unifiedEndIndex
375 * @return Array of buckets in the specified order
377 public DateBucket[] getBuckets(SortOrder order) {
378 MtpDeviceIndexRunnable.Results results = mResults;
379 if (results == null) {
382 return (order == SortOrder.ASCENDING) ? results.buckets : results.reversedBuckets;
385 protected void resetState() {
393 * @return whether the index is at the given generation and the given device is connected
395 protected boolean isAtGeneration(MtpDevice device, long generation) {
396 return (mGeneration == generation) && (mDevice == device);
399 protected synchronized boolean setIndexingResults(MtpDevice device, long generation,
400 MtpDeviceIndexRunnable.Results results) {
401 if (!isAtGeneration(device, generation)) {
405 onIndexFinish(true /*successful*/);
409 protected synchronized void onIndexFinish(boolean successful) {
413 if (mProgressListener != null) {
414 mProgressListener.onIndexingFinished();
418 protected synchronized void onSorting() {
419 if (mProgressListener != null) {
420 mProgressListener.onSortingStarted();
424 protected synchronized void onObjectIndexed(IngestObjectInfo object, int numVisited) {
425 if (mProgressListener != null) {
426 mProgressListener.onObjectIndexed(object, numVisited);
430 protected long getGeneration() {