| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.gallery3d.ingest; |
| |
| import android.mtp.MtpConstants; |
| import android.mtp.MtpDevice; |
| import android.mtp.MtpObjectInfo; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Stack; |
| |
| /** |
| * MTP objects in the index are organized into "buckets," or groupings. |
| * At present, these buckets are based on the date an item was created. |
| * |
| * When the index is created, the buckets are sorted in their natural |
| * order, and the items within the buckets sorted by the date they are taken. |
| * |
| * The index enables the access of items and bucket labels as one unified list. |
| * For example, let's say we have the following data in the index: |
| * [Bucket A]: [photo 1], [photo 2] |
| * [Bucket B]: [photo 3] |
| * |
| * Then the items can be thought of as being organized as a 5 element list: |
| * [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3] |
| * |
| * The data can also be accessed in descending order, in which case the list |
| * would be a bit different from simply reversing the ascending list, since the |
| * bucket labels need to always be at the beginning: |
| * [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1] |
| * |
| * The index enables all the following operations in constant time, both for |
| * ascending and descending views of the data: |
| * - get/getAscending/getDescending: get an item at a specified list position |
| * - size: get the total number of items (bucket labels and MTP objects) |
| * - getFirstPositionForBucketNumber |
| * - getBucketNumberForPosition |
| * - isFirstInBucket |
| * |
| * See the comments in buildLookupIndex for implementation notes. |
| */ |
| public class MtpDeviceIndex { |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId()); |
| result = prime * result + mGeneration; |
| return result; |
| } |
| |
| public interface ProgressListener { |
| public void onObjectIndexed(MtpObjectInfo object, int numVisited); |
| |
| public void onSorting(); |
| |
| public void onIndexFinish(); |
| } |
| |
| public enum SortOrder { |
| Ascending, Descending |
| } |
| |
| private MtpDevice mDevice; |
| private int[] mUnifiedLookupIndex; |
| private MtpObjectInfo[] mMtpObjects; |
| private DateBucket[] mBuckets; |
| private int mGeneration = 0; |
| |
| public enum Progress { |
| Uninitialized, Initialized, Pending, Started, Sorting, Finished |
| } |
| |
| private Progress mProgress = Progress.Uninitialized; |
| private ProgressListener mProgressListener; |
| |
| private static final MtpDeviceIndex sInstance = new MtpDeviceIndex(); |
| private static final MtpObjectTimestampComparator sMtpObjectComparator = |
| new MtpObjectTimestampComparator(); |
| |
| public static MtpDeviceIndex getInstance() { |
| return sInstance; |
| } |
| |
| private MtpDeviceIndex() { |
| } |
| |
| synchronized public MtpDevice getDevice() { |
| return mDevice; |
| } |
| |
| /** |
| * Sets the MtpDevice that should be indexed and initializes state, but does |
| * not kick off the actual indexing task, which is instead done by using |
| * {@link #getIndexRunnable()} |
| * |
| * @param device The MtpDevice that should be indexed |
| */ |
| synchronized public void setDevice(MtpDevice device) { |
| if (device == mDevice) return; |
| mDevice = device; |
| resetState(); |
| } |
| |
| /** |
| * Provides a Runnable for the indexing task assuming the state has already |
| * been correctly initialized (by calling {@link #setDevice(MtpDevice)}) and |
| * has not already been run. |
| * |
| * @return Runnable for the main indexing task |
| */ |
| synchronized public Runnable getIndexRunnable() { |
| if (mProgress != Progress.Initialized) return null; |
| mProgress = Progress.Pending; |
| return new IndexRunnable(mDevice); |
| } |
| |
| synchronized public boolean indexReady() { |
| return mProgress == Progress.Finished; |
| } |
| |
| synchronized public Progress getProgress() { |
| return mProgress; |
| } |
| |
| /** |
| * @param listener Listener to change to |
| * @return Progress at the time the listener was added (useful for |
| * configuring initial UI state) |
| */ |
| synchronized public Progress setProgressListener(ProgressListener listener) { |
| mProgressListener = listener; |
| return mProgress; |
| } |
| |
| /** |
| * Make the listener null if it matches the argument |
| * |
| * @param listener Listener to unset, if currently registered |
| */ |
| synchronized public void unsetProgressListener(ProgressListener listener) { |
| if (mProgressListener == listener) |
| mProgressListener = null; |
| } |
| |
| /** |
| * @return The total number of elements in the index (labels and items) |
| */ |
| public int size() { |
| return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0; |
| } |
| |
| /** |
| * @param position Index of item to fetch, where 0 is the first item in the |
| * specified order |
| * @param order |
| * @return the bucket label or MtpObjectInfo at the specified position and |
| * order |
| */ |
| public Object get(int position, SortOrder order) { |
| if(order == SortOrder.Ascending) { |
| return getAscending(position); |
| } else { |
| return getDescending(position); |
| } |
| } |
| |
| /** |
| * @param position Index of item to fetch, where 0 is the first item in |
| * ascending order |
| * @return position-th item in ascending order |
| */ |
| public Object getAscending(int position) { |
| if (mProgress != Progress.Finished) return null; |
| DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]]; |
| if (bucket.unifiedStartIndex == position) { |
| return bucket.bucket; |
| } else { |
| return bucket.get(position - 1 - bucket.unifiedStartIndex); |
| } |
| } |
| |
| /** |
| * @param position Index of item to fetch, where 0 is the last item in |
| * ascending order |
| * @return position-th item in descending order |
| */ |
| public Object getDescending(int position) { |
| if (mProgress != Progress.Finished) return null; |
| int zeroIndex = mUnifiedLookupIndex.length - 1 - position; |
| DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]]; |
| if (bucket.unifiedEndIndex == zeroIndex) { |
| return bucket.bucket; |
| } else { |
| return bucket.get(zeroIndex - bucket.unifiedStartIndex); |
| } |
| } |
| |
| /** |
| * @param position Index of item to fetch from a view of the data that doesn't |
| * include labels and is in ascending order |
| * @return position-th item in ascending order, when not including labels |
| */ |
| public MtpObjectInfo getWithoutLabels(int position, SortOrder order) { |
| if (mProgress != Progress.Finished) return null; |
| if (order == SortOrder.Ascending) { |
| return mMtpObjects[position]; |
| } else { |
| return mMtpObjects[mMtpObjects.length - 1 - position]; |
| } |
| } |
| |
| /** |
| * @return The number of MTP items in the index (without labels) |
| */ |
| public int sizeWithoutLabels() { |
| return mProgress == Progress.Finished ? mMtpObjects.length : 0; |
| } |
| |
| public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) { |
| if (order == SortOrder.Ascending) { |
| return mBuckets[bucketNumber].unifiedStartIndex; |
| } else { |
| return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1; |
| } |
| } |
| |
| public int getBucketNumberForPosition(int position, SortOrder order) { |
| if (order == SortOrder.Ascending) { |
| return mUnifiedLookupIndex[position]; |
| } else { |
| return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position]; |
| } |
| } |
| |
| public boolean isFirstInBucket(int position, SortOrder order) { |
| if (order == SortOrder.Ascending) { |
| return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position; |
| } else { |
| position = mUnifiedLookupIndex.length - 1 - position; |
| return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position; |
| } |
| } |
| |
| private Object[] mCachedReverseBuckets; |
| |
| public Object[] getBuckets(SortOrder order) { |
| if (mBuckets == null) return null; |
| if (order == SortOrder.Ascending) { |
| return mBuckets; |
| } else { |
| if (mCachedReverseBuckets == null) { |
| computeReversedBuckets(); |
| } |
| return mCachedReverseBuckets; |
| } |
| } |
| |
| /* |
| * See the comments for buildLookupIndex for notes on the specific fields of |
| * this class. |
| */ |
| private class DateBucket implements Comparable<DateBucket> { |
| SimpleDate bucket; |
| List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>(); |
| int unifiedStartIndex; |
| int unifiedEndIndex; |
| int itemsStartIndex; |
| |
| public DateBucket(SimpleDate bucket) { |
| this.bucket = bucket; |
| } |
| |
| public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) { |
| this(bucket); |
| tempElementsList.add(firstElement); |
| } |
| |
| void sortElements(Comparator<MtpObjectInfo> comparator) { |
| Collections.sort(tempElementsList, comparator); |
| } |
| |
| public MtpObjectInfo get(int position) { |
| return mMtpObjects[itemsStartIndex + position]; |
| } |
| |
| @Override |
| public String toString() { |
| return bucket.toString(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return bucket.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) return true; |
| if (obj == null) return false; |
| if (!(obj instanceof DateBucket)) return false; |
| DateBucket other = (DateBucket) obj; |
| if (bucket == null) { |
| if (other.bucket != null) return false; |
| } else if (!bucket.equals(other.bucket)) { |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public int compareTo(DateBucket another) { |
| return this.bucket.compareTo(another.bucket); |
| } |
| } |
| |
| /** |
| * Comparator to sort MtpObjectInfo objects by date created. |
| */ |
| private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> { |
| @Override |
| public int compare(MtpObjectInfo o1, MtpObjectInfo o2) { |
| long diff = o1.getDateCreated() - o2.getDateCreated(); |
| if (diff < 0) { |
| return -1; |
| } else if (diff == 0) { |
| return 0; |
| } else { |
| return 1; |
| } |
| } |
| } |
| |
| private void resetState() { |
| mGeneration++; |
| mUnifiedLookupIndex = null; |
| mMtpObjects = null; |
| mBuckets = null; |
| mCachedReverseBuckets = null; |
| mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized; |
| } |
| |
| |
| private class IndexRunnable implements Runnable { |
| private int[] mUnifiedLookupIndex; |
| private MtpObjectInfo[] mMtpObjects; |
| private DateBucket[] mBuckets; |
| private Map<SimpleDate, DateBucket> mBucketsTemp; |
| private MtpDevice mDevice; |
| private int mNumObjects = 0; |
| |
| private class IndexingException extends Exception {}; |
| |
| public IndexRunnable(MtpDevice device) { |
| mDevice = device; |
| } |
| |
| /* |
| * Implementation note: this is the way the index supports a lot of its operations in |
| * constant time and respecting the need to have bucket names always come before items |
| * in that bucket when accessing the list sequentially, both in ascending and descending |
| * orders. |
| * |
| * Let's say the data we have in the index is the following: |
| * [Bucket A]: [photo 1], [photo 2] |
| * [Bucket B]: [photo 3] |
| * |
| * In this case, the lookup index array would be |
| * [0, 0, 0, 1, 1] |
| * |
| * Now, whether we access the list in ascending or descending order, we know which bucket |
| * to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first |
| * item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex |
| * that correspond to indices in this lookup index array, allowing us to calculate the |
| * offset of the specific item we want from within a specific bucket. |
| */ |
| private void buildLookupIndex() { |
| int numBuckets = mBuckets.length; |
| mUnifiedLookupIndex = new int[mNumObjects + numBuckets]; |
| int currentUnifiedIndexEntry = 0; |
| int nextUnifiedEntry; |
| |
| mMtpObjects = new MtpObjectInfo[mNumObjects]; |
| int currentItemsEntry = 0; |
| for (int i = 0; i < numBuckets; i++) { |
| DateBucket bucket = mBuckets[i]; |
| nextUnifiedEntry = currentUnifiedIndexEntry + bucket.tempElementsList.size() + 1; |
| Arrays.fill(mUnifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i); |
| bucket.unifiedStartIndex = currentUnifiedIndexEntry; |
| bucket.unifiedEndIndex = nextUnifiedEntry - 1; |
| currentUnifiedIndexEntry = nextUnifiedEntry; |
| |
| bucket.itemsStartIndex = currentItemsEntry; |
| for (int j = 0; j < bucket.tempElementsList.size(); j++) { |
| mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j); |
| currentItemsEntry++; |
| } |
| bucket.tempElementsList = null; |
| } |
| } |
| |
| private void copyResults() { |
| MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex; |
| MtpDeviceIndex.this.mMtpObjects = mMtpObjects; |
| MtpDeviceIndex.this.mBuckets = mBuckets; |
| mUnifiedLookupIndex = null; |
| mMtpObjects = null; |
| mBuckets = null; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| indexDevice(); |
| } catch (IndexingException e) { |
| synchronized (MtpDeviceIndex.this) { |
| resetState(); |
| if (mProgressListener != null) { |
| mProgressListener.onIndexFinish(); |
| } |
| } |
| } |
| } |
| |
| private void indexDevice() throws IndexingException { |
| synchronized (MtpDeviceIndex.this) { |
| mProgress = Progress.Started; |
| } |
| mBucketsTemp = new HashMap<SimpleDate, DateBucket>(); |
| for (int storageId : mDevice.getStorageIds()) { |
| if (mDevice != getDevice()) throw new IndexingException(); |
| Stack<Integer> pendingDirectories = new Stack<Integer>(); |
| pendingDirectories.add(0xFFFFFFFF); // start at the root of the device |
| while (!pendingDirectories.isEmpty()) { |
| if (mDevice != getDevice()) throw new IndexingException(); |
| int dirHandle = pendingDirectories.pop(); |
| for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) { |
| MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle); |
| if (objectInfo == null) throw new IndexingException(); |
| switch (objectInfo.getFormat()) { |
| case MtpConstants.FORMAT_JFIF: |
| case MtpConstants.FORMAT_EXIF_JPEG: |
| addObject(objectInfo); |
| break; |
| case MtpConstants.FORMAT_ASSOCIATION: |
| pendingDirectories.add(objectHandle); |
| break; |
| } |
| } |
| } |
| } |
| Collection<DateBucket> values = mBucketsTemp.values(); |
| mBucketsTemp = null; |
| mBuckets = values.toArray(new DateBucket[values.size()]); |
| values = null; |
| synchronized (MtpDeviceIndex.this) { |
| mProgress = Progress.Sorting; |
| if (mProgressListener != null) { |
| mProgressListener.onSorting(); |
| } |
| } |
| sortAll(); |
| buildLookupIndex(); |
| synchronized (MtpDeviceIndex.this) { |
| if (mDevice != getDevice()) throw new IndexingException(); |
| copyResults(); |
| |
| /* |
| * In order for getBuckets to operate in constant time for descending |
| * order, we must precompute a reversed array of the buckets, mainly |
| * because the android.widget.SectionIndexer interface which adapters |
| * that call getBuckets implement depends on section numbers to be |
| * ascending relative to the scroll position, so we must have this for |
| * descending order or the scrollbar goes crazy. |
| */ |
| computeReversedBuckets(); |
| |
| mProgress = Progress.Finished; |
| if (mProgressListener != null) { |
| mProgressListener.onIndexFinish(); |
| } |
| } |
| } |
| |
| private SimpleDate mDateInstance = new SimpleDate(); |
| |
| private void addObject(MtpObjectInfo objectInfo) { |
| mNumObjects++; |
| mDateInstance.setTimestamp(objectInfo.getDateCreated()); |
| DateBucket bucket = mBucketsTemp.get(mDateInstance); |
| if (bucket == null) { |
| bucket = new DateBucket(mDateInstance, objectInfo); |
| mBucketsTemp.put(mDateInstance, bucket); |
| mDateInstance = new SimpleDate(); // only create new date |
| // objects when they are used |
| return; |
| } else { |
| bucket.tempElementsList.add(objectInfo); |
| } |
| if (mProgressListener != null) { |
| mProgressListener.onObjectIndexed(objectInfo, mNumObjects); |
| } |
| } |
| |
| private void sortAll() { |
| Arrays.sort(mBuckets); |
| for (DateBucket bucket : mBuckets) { |
| bucket.sortElements(sMtpObjectComparator); |
| } |
| } |
| |
| } |
| |
| private void computeReversedBuckets() { |
| mCachedReverseBuckets = new Object[mBuckets.length]; |
| for (int i = 0; i < mCachedReverseBuckets.length; i++) { |
| mCachedReverseBuckets[i] = mBuckets[mBuckets.length - 1 - i]; |
| } |
| } |
| } |