2 * Copyright (C) 2013 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.gallery3d.ingest;
19 import android.mtp.MtpConstants;
20 import android.mtp.MtpDevice;
21 import android.mtp.MtpObjectInfo;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.Comparator;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
33 import java.util.Stack;
36 * MTP objects in the index are organized into "buckets," or groupings.
37 * At present, these buckets are based on the date an item was created.
39 * When the index is created, the buckets are sorted in their natural
40 * order, and the items within the buckets sorted by the date they are taken.
42 * The index enables the access of items and bucket labels as one unified list.
43 * For example, let's say we have the following data in the index:
44 * [Bucket A]: [photo 1], [photo 2]
45 * [Bucket B]: [photo 3]
47 * Then the items can be thought of as being organized as a 5 element list:
48 * [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3]
50 * The data can also be accessed in descending order, in which case the list
51 * would be a bit different from simply reversing the ascending list, since the
52 * bucket labels need to always be at the beginning:
53 * [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1]
55 * The index enables all the following operations in constant time, both for
56 * ascending and descending views of the data:
57 * - get/getAscending/getDescending: get an item at a specified list position
58 * - size: get the total number of items (bucket labels and MTP objects)
59 * - getFirstPositionForBucketNumber
60 * - getBucketNumberForPosition
63 * See the comments in buildLookupIndex for implementation notes.
65 public class MtpDeviceIndex {
67 public static final int FORMAT_MOV = 0x300D; // For some reason this is not in MtpConstants
69 public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
70 public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
73 SUPPORTED_IMAGE_FORMATS = new HashSet<Integer>();
74 SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_JFIF);
75 SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_EXIF_JPEG);
76 SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_PNG);
77 SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_GIF);
78 SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_BMP);
80 SUPPORTED_VIDEO_FORMATS = new HashSet<Integer>();
81 SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_3GP_CONTAINER);
82 SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_AVI);
83 SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MP4_CONTAINER);
84 SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MPEG);
85 // TODO: add FORMAT_MOV once Media Scanner supports .mov files
89 public int hashCode() {
92 result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId());
93 result = prime * result + mGeneration;
97 public interface ProgressListener {
98 public void onObjectIndexed(MtpObjectInfo object, int numVisited);
100 public void onSorting();
102 public void onIndexFinish();
105 public enum SortOrder {
106 Ascending, Descending
109 private MtpDevice mDevice;
110 private int[] mUnifiedLookupIndex;
111 private MtpObjectInfo[] mMtpObjects;
112 private DateBucket[] mBuckets;
113 private int mGeneration = 0;
115 public enum Progress {
116 Uninitialized, Initialized, Pending, Started, Sorting, Finished
119 private Progress mProgress = Progress.Uninitialized;
120 private ProgressListener mProgressListener;
122 private static final MtpDeviceIndex sInstance = new MtpDeviceIndex();
123 private static final MtpObjectTimestampComparator sMtpObjectComparator =
124 new MtpObjectTimestampComparator();
126 public static MtpDeviceIndex getInstance() {
130 private MtpDeviceIndex() {
133 synchronized public MtpDevice getDevice() {
138 * Sets the MtpDevice that should be indexed and initializes state, but does
139 * not kick off the actual indexing task, which is instead done by using
140 * {@link #getIndexRunnable()}
142 * @param device The MtpDevice that should be indexed
144 synchronized public void setDevice(MtpDevice device) {
145 if (device == mDevice) return;
151 * Provides a Runnable for the indexing task assuming the state has already
152 * been correctly initialized (by calling {@link #setDevice(MtpDevice)}) and
153 * has not already been run.
155 * @return Runnable for the main indexing task
157 synchronized public Runnable getIndexRunnable() {
158 if (mProgress != Progress.Initialized) return null;
159 mProgress = Progress.Pending;
160 return new IndexRunnable(mDevice);
163 synchronized public boolean indexReady() {
164 return mProgress == Progress.Finished;
167 synchronized public Progress getProgress() {
172 * @param listener Listener to change to
173 * @return Progress at the time the listener was added (useful for
174 * configuring initial UI state)
176 synchronized public Progress setProgressListener(ProgressListener listener) {
177 mProgressListener = listener;
182 * Make the listener null if it matches the argument
184 * @param listener Listener to unset, if currently registered
186 synchronized public void unsetProgressListener(ProgressListener listener) {
187 if (mProgressListener == listener)
188 mProgressListener = null;
192 * @return The total number of elements in the index (labels and items)
195 return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0;
199 * @param position Index of item to fetch, where 0 is the first item in the
202 * @return the bucket label or MtpObjectInfo at the specified position and
205 public Object get(int position, SortOrder order) {
206 if (mProgress != Progress.Finished) return null;
207 if(order == SortOrder.Ascending) {
208 DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
209 if (bucket.unifiedStartIndex == position) {
210 return bucket.bucket;
212 return mMtpObjects[bucket.itemsStartIndex + position - 1
213 - bucket.unifiedStartIndex];
216 int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
217 DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
218 if (bucket.unifiedEndIndex == zeroIndex) {
219 return bucket.bucket;
221 return mMtpObjects[bucket.itemsStartIndex + zeroIndex
222 - bucket.unifiedStartIndex];
228 * @param position Index of item to fetch from a view of the data that doesn't
229 * include labels and is in the specified order
230 * @return position-th item in specified order, when not including labels
232 public MtpObjectInfo getWithoutLabels(int position, SortOrder order) {
233 if (mProgress != Progress.Finished) return null;
234 if (order == SortOrder.Ascending) {
235 return mMtpObjects[position];
237 return mMtpObjects[mMtpObjects.length - 1 - position];
242 * Although this is O(log(number of buckets)), and thus should not be used
243 * in hotspots, even if the attached device has items for every day for
244 * a five-year timeframe, it would still only take 11 iterations at most,
245 * so shouldn't be a huge issue.
246 * @param position Index of item to map from a view of the data that doesn't
247 * include labels and is in the specified order
249 * @return position in a view of the data that does include labels
251 public int getPositionFromPositionWithoutLabels(int position, SortOrder order) {
252 if (mProgress != Progress.Finished) return -1;
253 if (order == SortOrder.Descending) {
254 position = mMtpObjects.length - 1 - position;
256 int bucketNumber = 0;
258 int iMax = mBuckets.length - 1;
259 while (iMax >= iMin) {
260 int iMid = (iMax + iMin) / 2;
261 if (mBuckets[iMid].itemsStartIndex + mBuckets[iMid].numItems <= position) {
263 } else if (mBuckets[iMid].itemsStartIndex > position) {
270 if (mBuckets.length == 0 || mUnifiedLookupIndex.length == 0) {
273 int mappedPos = mBuckets[bucketNumber].unifiedStartIndex
274 + position - mBuckets[bucketNumber].itemsStartIndex;
275 if (order == SortOrder.Descending) {
276 mappedPos = mUnifiedLookupIndex.length - 1 - mappedPos;
281 public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
282 if (mProgress != Progress.Finished) return -1;
283 if(order == SortOrder.Ascending) {
284 DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
285 if (bucket.unifiedStartIndex == position) position++;
286 return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
288 int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
289 if (mBuckets.length == 0 || mUnifiedLookupIndex.length == 0) {
292 DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
293 if (bucket.unifiedEndIndex == zeroIndex) zeroIndex--;
294 return mMtpObjects.length - 1 - bucket.itemsStartIndex
295 - zeroIndex + bucket.unifiedStartIndex;
300 * @return The number of MTP items in the index (without labels)
302 public int sizeWithoutLabels() {
303 return mProgress == Progress.Finished ? mMtpObjects.length : 0;
306 public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
307 if (order == SortOrder.Ascending) {
308 return mBuckets[bucketNumber].unifiedStartIndex;
310 return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1;
314 public int getBucketNumberForPosition(int position, SortOrder order) {
315 if (order == SortOrder.Ascending) {
316 return mUnifiedLookupIndex[position];
318 return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position];
322 public boolean isFirstInBucket(int position, SortOrder order) {
323 if (order == SortOrder.Ascending) {
324 return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position;
326 position = mUnifiedLookupIndex.length - 1 - position;
327 return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position;
331 private Object[] mCachedReverseBuckets;
333 public Object[] getBuckets(SortOrder order) {
334 if (mBuckets == null) return null;
335 if (order == SortOrder.Ascending) {
338 if (mCachedReverseBuckets == null) {
339 computeReversedBuckets();
341 return mCachedReverseBuckets;
346 * See the comments for buildLookupIndex for notes on the specific fields of
349 private class DateBucket implements Comparable<DateBucket> {
351 List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>();
352 int unifiedStartIndex;
357 public DateBucket(SimpleDate bucket) {
358 this.bucket = bucket;
361 public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) {
363 tempElementsList.add(firstElement);
366 void sortElements(Comparator<MtpObjectInfo> comparator) {
367 Collections.sort(tempElementsList, comparator);
371 public String toString() {
372 return bucket.toString();
376 public int hashCode() {
377 return bucket.hashCode();
381 public boolean equals(Object obj) {
382 if (this == obj) return true;
383 if (obj == null) return false;
384 if (!(obj instanceof DateBucket)) return false;
385 DateBucket other = (DateBucket) obj;
386 if (bucket == null) {
387 if (other.bucket != null) return false;
388 } else if (!bucket.equals(other.bucket)) {
395 public int compareTo(DateBucket another) {
396 return this.bucket.compareTo(another.bucket);
401 * Comparator to sort MtpObjectInfo objects by date created.
403 private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> {
405 public int compare(MtpObjectInfo o1, MtpObjectInfo o2) {
406 long diff = o1.getDateCreated() - o2.getDateCreated();
409 } else if (diff == 0) {
417 private void resetState() {
419 mUnifiedLookupIndex = null;
422 mCachedReverseBuckets = null;
423 mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized;
427 private class IndexRunnable implements Runnable {
428 private int[] mUnifiedLookupIndex;
429 private MtpObjectInfo[] mMtpObjects;
430 private DateBucket[] mBuckets;
431 private Map<SimpleDate, DateBucket> mBucketsTemp;
432 private MtpDevice mDevice;
433 private int mNumObjects = 0;
435 private class IndexingException extends Exception {};
437 public IndexRunnable(MtpDevice device) {
442 * Implementation note: this is the way the index supports a lot of its operations in
443 * constant time and respecting the need to have bucket names always come before items
444 * in that bucket when accessing the list sequentially, both in ascending and descending
447 * Let's say the data we have in the index is the following:
448 * [Bucket A]: [photo 1], [photo 2]
449 * [Bucket B]: [photo 3]
451 * In this case, the lookup index array would be
454 * Now, whether we access the list in ascending or descending order, we know which bucket
455 * to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first
456 * item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex
457 * that correspond to indices in this lookup index array, allowing us to calculate the
458 * offset of the specific item we want from within a specific bucket.
460 private void buildLookupIndex() {
461 int numBuckets = mBuckets.length;
462 mUnifiedLookupIndex = new int[mNumObjects + numBuckets];
463 int currentUnifiedIndexEntry = 0;
464 int nextUnifiedEntry;
466 mMtpObjects = new MtpObjectInfo[mNumObjects];
467 int currentItemsEntry = 0;
468 for (int i = 0; i < numBuckets; i++) {
469 DateBucket bucket = mBuckets[i];
470 nextUnifiedEntry = currentUnifiedIndexEntry + bucket.tempElementsList.size() + 1;
471 Arrays.fill(mUnifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i);
472 bucket.unifiedStartIndex = currentUnifiedIndexEntry;
473 bucket.unifiedEndIndex = nextUnifiedEntry - 1;
474 currentUnifiedIndexEntry = nextUnifiedEntry;
476 bucket.itemsStartIndex = currentItemsEntry;
477 bucket.numItems = bucket.tempElementsList.size();
478 for (int j = 0; j < bucket.numItems; j++) {
479 mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j);
482 bucket.tempElementsList = null;
486 private void copyResults() {
487 MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex;
488 MtpDeviceIndex.this.mMtpObjects = mMtpObjects;
489 MtpDeviceIndex.this.mBuckets = mBuckets;
490 mUnifiedLookupIndex = null;
499 } catch (IndexingException e) {
500 synchronized (MtpDeviceIndex.this) {
502 if (mProgressListener != null) {
503 mProgressListener.onIndexFinish();
509 private void indexDevice() throws IndexingException {
510 synchronized (MtpDeviceIndex.this) {
511 mProgress = Progress.Started;
513 mBucketsTemp = new HashMap<SimpleDate, DateBucket>();
514 for (int storageId : mDevice.getStorageIds()) {
515 if (mDevice != getDevice()) throw new IndexingException();
516 Stack<Integer> pendingDirectories = new Stack<Integer>();
517 pendingDirectories.add(0xFFFFFFFF); // start at the root of the device
518 while (!pendingDirectories.isEmpty()) {
519 if (mDevice != getDevice()) throw new IndexingException();
520 int dirHandle = pendingDirectories.pop();
521 for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) {
522 MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle);
523 if (objectInfo == null) throw new IndexingException();
524 int format = objectInfo.getFormat();
525 if (format == MtpConstants.FORMAT_ASSOCIATION) {
526 pendingDirectories.add(objectHandle);
527 } else if (SUPPORTED_IMAGE_FORMATS.contains(format)
528 || SUPPORTED_VIDEO_FORMATS.contains(format)) {
529 addObject(objectInfo);
534 Collection<DateBucket> values = mBucketsTemp.values();
536 mBuckets = values.toArray(new DateBucket[values.size()]);
538 synchronized (MtpDeviceIndex.this) {
539 mProgress = Progress.Sorting;
540 if (mProgressListener != null) {
541 mProgressListener.onSorting();
546 synchronized (MtpDeviceIndex.this) {
547 if (mDevice != getDevice()) throw new IndexingException();
551 * In order for getBuckets to operate in constant time for descending
552 * order, we must precompute a reversed array of the buckets, mainly
553 * because the android.widget.SectionIndexer interface which adapters
554 * that call getBuckets implement depends on section numbers to be
555 * ascending relative to the scroll position, so we must have this for
556 * descending order or the scrollbar goes crazy.
558 computeReversedBuckets();
560 mProgress = Progress.Finished;
561 if (mProgressListener != null) {
562 mProgressListener.onIndexFinish();
567 private SimpleDate mDateInstance = new SimpleDate();
569 private void addObject(MtpObjectInfo objectInfo) {
571 mDateInstance.setTimestamp(objectInfo.getDateCreated());
572 DateBucket bucket = mBucketsTemp.get(mDateInstance);
573 if (bucket == null) {
574 bucket = new DateBucket(mDateInstance, objectInfo);
575 mBucketsTemp.put(mDateInstance, bucket);
576 mDateInstance = new SimpleDate(); // only create new date
577 // objects when they are used
580 bucket.tempElementsList.add(objectInfo);
582 if (mProgressListener != null) {
583 mProgressListener.onObjectIndexed(objectInfo, mNumObjects);
587 private void sortAll() {
588 Arrays.sort(mBuckets);
589 for (DateBucket bucket : mBuckets) {
590 bucket.sortElements(sMtpObjectComparator);
596 private void computeReversedBuckets() {
597 mCachedReverseBuckets = new Object[mBuckets.length];
598 for (int i = 0; i < mCachedReverseBuckets.length; i++) {
599 mCachedReverseBuckets[i] = mBuckets[mBuckets.length - 1 - i];