Home | History | Annotate | Download | only in ingest
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      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
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     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.
     15  */
     16 
     17 package com.android.gallery3d.ingest;
     18 
     19 import android.mtp.MtpConstants;
     20 import android.mtp.MtpDevice;
     21 import android.mtp.MtpObjectInfo;
     22 
     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.List;
     30 import java.util.Map;
     31 import java.util.Stack;
     32 
     33 /**
     34  * MTP objects in the index are organized into "buckets," or groupings.
     35  * At present, these buckets are based on the date an item was created.
     36  *
     37  * When the index is created, the buckets are sorted in their natural
     38  * order, and the items within the buckets sorted by the date they are taken.
     39  *
     40  * The index enables the access of items and bucket labels as one unified list.
     41  * For example, let's say we have the following data in the index:
     42  *    [Bucket A]: [photo 1], [photo 2]
     43  *    [Bucket B]: [photo 3]
     44  *
     45  * Then the items can be thought of as being organized as a 5 element list:
     46  *   [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3]
     47  *
     48  * The data can also be accessed in descending order, in which case the list
     49  * would be a bit different from simply reversing the ascending list, since the
     50  * bucket labels need to always be at the beginning:
     51  *   [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1]
     52  *
     53  * The index enables all the following operations in constant time, both for
     54  * ascending and descending views of the data:
     55  *   - get/getAscending/getDescending: get an item at a specified list position
     56  *   - size: get the total number of items (bucket labels and MTP objects)
     57  *   - getFirstPositionForBucketNumber
     58  *   - getBucketNumberForPosition
     59  *   - isFirstInBucket
     60  *
     61  * See the comments in buildLookupIndex for implementation notes.
     62  */
     63 public class MtpDeviceIndex {
     64 
     65     @Override
     66     public int hashCode() {
     67         final int prime = 31;
     68         int result = 1;
     69         result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId());
     70         result = prime * result + mGeneration;
     71         return result;
     72     }
     73 
     74     public interface ProgressListener {
     75         public void onObjectIndexed(MtpObjectInfo object, int numVisited);
     76 
     77         public void onSorting();
     78 
     79         public void onIndexFinish();
     80     }
     81 
     82     public enum SortOrder {
     83         Ascending, Descending
     84     }
     85 
     86     private MtpDevice mDevice;
     87     private int[] mUnifiedLookupIndex;
     88     private MtpObjectInfo[] mMtpObjects;
     89     private DateBucket[] mBuckets;
     90     private int mGeneration = 0;
     91 
     92     public enum Progress {
     93         Uninitialized, Initialized, Pending, Started, Sorting, Finished
     94     }
     95 
     96     private Progress mProgress = Progress.Uninitialized;
     97     private ProgressListener mProgressListener;
     98 
     99     private static final MtpDeviceIndex sInstance = new MtpDeviceIndex();
    100     private static final MtpObjectTimestampComparator sMtpObjectComparator =
    101             new MtpObjectTimestampComparator();
    102 
    103     public static MtpDeviceIndex getInstance() {
    104         return sInstance;
    105     }
    106 
    107     private MtpDeviceIndex() {
    108     }
    109 
    110     synchronized public MtpDevice getDevice() {
    111         return mDevice;
    112     }
    113 
    114     /**
    115      * Sets the MtpDevice that should be indexed and initializes state, but does
    116      * not kick off the actual indexing task, which is instead done by using
    117      * {@link #getIndexRunnable()}
    118      *
    119      * @param device The MtpDevice that should be indexed
    120      */
    121     synchronized public void setDevice(MtpDevice device) {
    122         if (device == mDevice) return;
    123         mDevice = device;
    124         resetState();
    125     }
    126 
    127     /**
    128      * Provides a Runnable for the indexing task assuming the state has already
    129      * been correctly initialized (by calling {@link #setDevice(MtpDevice)}) and
    130      * has not already been run.
    131      *
    132      * @return Runnable for the main indexing task
    133      */
    134     synchronized public Runnable getIndexRunnable() {
    135         if (mProgress != Progress.Initialized) return null;
    136         mProgress = Progress.Pending;
    137         return new IndexRunnable(mDevice);
    138     }
    139 
    140     synchronized public boolean indexReady() {
    141         return mProgress == Progress.Finished;
    142     }
    143 
    144     synchronized public Progress getProgress() {
    145         return mProgress;
    146     }
    147 
    148     /**
    149      * @param listener Listener to change to
    150      * @return Progress at the time the listener was added (useful for
    151      *         configuring initial UI state)
    152      */
    153     synchronized public Progress setProgressListener(ProgressListener listener) {
    154         mProgressListener = listener;
    155         return mProgress;
    156     }
    157 
    158     /**
    159      * Make the listener null if it matches the argument
    160      *
    161      * @param listener Listener to unset, if currently registered
    162      */
    163     synchronized public void unsetProgressListener(ProgressListener listener) {
    164         if (mProgressListener == listener)
    165             mProgressListener = null;
    166     }
    167 
    168     /**
    169      * @return The total number of elements in the index (labels and items)
    170      */
    171     public int size() {
    172         return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0;
    173     }
    174 
    175     /**
    176      * @param position Index of item to fetch, where 0 is the first item in the
    177      *            specified order
    178      * @param order
    179      * @return the bucket label or MtpObjectInfo at the specified position and
    180      *         order
    181      */
    182     public Object get(int position, SortOrder order) {
    183         if (mProgress != Progress.Finished) return null;
    184         if(order == SortOrder.Ascending) {
    185             DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
    186             if (bucket.unifiedStartIndex == position) {
    187                 return bucket.bucket;
    188             } else {
    189                 return mMtpObjects[bucket.itemsStartIndex + position - 1
    190                                    - bucket.unifiedStartIndex];
    191             }
    192         } else {
    193             int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
    194             DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
    195             if (bucket.unifiedEndIndex == zeroIndex) {
    196                 return bucket.bucket;
    197             } else {
    198                 return mMtpObjects[bucket.itemsStartIndex + zeroIndex
    199                                    - bucket.unifiedStartIndex];
    200             }
    201         }
    202     }
    203 
    204     /**
    205      * @param position Index of item to fetch from a view of the data that doesn't
    206      *            include labels and is in the specified order
    207      * @return position-th item in specified order, when not including labels
    208      */
    209     public MtpObjectInfo getWithoutLabels(int position, SortOrder order) {
    210         if (mProgress != Progress.Finished) return null;
    211         if (order == SortOrder.Ascending) {
    212             return mMtpObjects[position];
    213         } else {
    214             return mMtpObjects[mMtpObjects.length - 1 - position];
    215         }
    216     }
    217 
    218     /**
    219      * Although this is O(log(number of buckets)), and thus should not be used
    220      * in hotspots, even if the attached device has items for every day for
    221      * a five-year timeframe, it would still only take 11 iterations at most,
    222      * so shouldn't be a huge issue.
    223      * @param position Index of item to map from a view of the data that doesn't
    224      *            include labels and is in the specified order
    225      * @param order
    226      * @return position in a view of the data that does include labels
    227      */
    228     public int getPositionFromPositionWithoutLabels(int position, SortOrder order) {
    229         if (mProgress != Progress.Finished) return -1;
    230         if (order == SortOrder.Descending) {
    231             position = mMtpObjects.length - 1 - position;
    232         }
    233         int bucketNumber = 0;
    234         int iMin = 0;
    235         int iMax = mBuckets.length - 1;
    236         while (iMax >= iMin) {
    237             int iMid = (iMax + iMin) / 2;
    238             if (mBuckets[iMid].itemsStartIndex + mBuckets[iMid].numItems <= position) {
    239                 iMin = iMid + 1;
    240             } else if (mBuckets[iMid].itemsStartIndex > position) {
    241                 iMax = iMid - 1;
    242             } else {
    243                 bucketNumber = iMid;
    244                 break;
    245             }
    246         }
    247         int mappedPos = mBuckets[bucketNumber].unifiedStartIndex
    248                 + position - mBuckets[bucketNumber].itemsStartIndex;
    249         if (order == SortOrder.Descending) {
    250             mappedPos = mUnifiedLookupIndex.length - 1 - mappedPos;
    251         }
    252         return mappedPos;
    253     }
    254 
    255     public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
    256         if (mProgress != Progress.Finished) return -1;
    257         if(order == SortOrder.Ascending) {
    258             DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
    259             if (bucket.unifiedStartIndex == position) position++;
    260             return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
    261         } else {
    262             int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
    263             DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
    264             if (bucket.unifiedEndIndex == zeroIndex) zeroIndex--;
    265             return mMtpObjects.length - 1 - bucket.itemsStartIndex
    266                     - zeroIndex + bucket.unifiedStartIndex;
    267         }
    268     }
    269 
    270     /**
    271      * @return The number of MTP items in the index (without labels)
    272      */
    273     public int sizeWithoutLabels() {
    274         return mProgress == Progress.Finished ? mMtpObjects.length : 0;
    275     }
    276 
    277     public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
    278         if (order == SortOrder.Ascending) {
    279             return mBuckets[bucketNumber].unifiedStartIndex;
    280         } else {
    281             return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1;
    282         }
    283     }
    284 
    285     public int getBucketNumberForPosition(int position, SortOrder order) {
    286         if (order == SortOrder.Ascending) {
    287             return mUnifiedLookupIndex[position];
    288         } else {
    289             return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position];
    290         }
    291     }
    292 
    293     public boolean isFirstInBucket(int position, SortOrder order) {
    294         if (order == SortOrder.Ascending) {
    295             return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position;
    296         } else {
    297             position = mUnifiedLookupIndex.length - 1 - position;
    298             return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position;
    299         }
    300     }
    301 
    302     private Object[] mCachedReverseBuckets;
    303 
    304     public Object[] getBuckets(SortOrder order) {
    305         if (mBuckets == null) return null;
    306         if (order == SortOrder.Ascending) {
    307             return mBuckets;
    308         } else {
    309             if (mCachedReverseBuckets == null) {
    310                 computeReversedBuckets();
    311             }
    312             return mCachedReverseBuckets;
    313         }
    314     }
    315 
    316     /*
    317      * See the comments for buildLookupIndex for notes on the specific fields of
    318      * this class.
    319      */
    320     private class DateBucket implements Comparable<DateBucket> {
    321         SimpleDate bucket;
    322         List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>();
    323         int unifiedStartIndex;
    324         int unifiedEndIndex;
    325         int itemsStartIndex;
    326         int numItems;
    327 
    328         public DateBucket(SimpleDate bucket) {
    329             this.bucket = bucket;
    330         }
    331 
    332         public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) {
    333             this(bucket);
    334             tempElementsList.add(firstElement);
    335         }
    336 
    337         void sortElements(Comparator<MtpObjectInfo> comparator) {
    338             Collections.sort(tempElementsList, comparator);
    339         }
    340 
    341         @Override
    342         public String toString() {
    343             return bucket.toString();
    344         }
    345 
    346         @Override
    347         public int hashCode() {
    348             return bucket.hashCode();
    349         }
    350 
    351         @Override
    352         public boolean equals(Object obj) {
    353             if (this == obj) return true;
    354             if (obj == null) return false;
    355             if (!(obj instanceof DateBucket)) return false;
    356             DateBucket other = (DateBucket) obj;
    357             if (bucket == null) {
    358                 if (other.bucket != null) return false;
    359             } else if (!bucket.equals(other.bucket)) {
    360                 return false;
    361             }
    362             return true;
    363         }
    364 
    365         @Override
    366         public int compareTo(DateBucket another) {
    367             return this.bucket.compareTo(another.bucket);
    368         }
    369     }
    370 
    371     /**
    372      * Comparator to sort MtpObjectInfo objects by date created.
    373      */
    374     private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> {
    375         @Override
    376         public int compare(MtpObjectInfo o1, MtpObjectInfo o2) {
    377             long diff = o1.getDateCreated() - o2.getDateCreated();
    378             if (diff < 0) {
    379                 return -1;
    380             } else if (diff == 0) {
    381                 return 0;
    382             } else {
    383                 return 1;
    384             }
    385         }
    386     }
    387 
    388     private void resetState() {
    389         mGeneration++;
    390         mUnifiedLookupIndex = null;
    391         mMtpObjects = null;
    392         mBuckets = null;
    393         mCachedReverseBuckets = null;
    394         mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized;
    395     }
    396 
    397 
    398     private class IndexRunnable implements Runnable {
    399         private int[] mUnifiedLookupIndex;
    400         private MtpObjectInfo[] mMtpObjects;
    401         private DateBucket[] mBuckets;
    402         private Map<SimpleDate, DateBucket> mBucketsTemp;
    403         private MtpDevice mDevice;
    404         private int mNumObjects = 0;
    405 
    406         private class IndexingException extends Exception {};
    407 
    408         public IndexRunnable(MtpDevice device) {
    409             mDevice = device;
    410         }
    411 
    412         /*
    413          * Implementation note: this is the way the index supports a lot of its operations in
    414          * constant time and respecting the need to have bucket names always come before items
    415          * in that bucket when accessing the list sequentially, both in ascending and descending
    416          * orders.
    417          *
    418          * Let's say the data we have in the index is the following:
    419          *  [Bucket A]: [photo 1], [photo 2]
    420          *  [Bucket B]: [photo 3]
    421          *
    422          *  In this case, the lookup index array would be
    423          *  [0, 0, 0, 1, 1]
    424          *
    425          *  Now, whether we access the list in ascending or descending order, we know which bucket
    426          *  to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first
    427          *  item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex
    428          *  that correspond to indices in this lookup index array, allowing us to calculate the
    429          *  offset of the specific item we want from within a specific bucket.
    430          */
    431         private void buildLookupIndex() {
    432             int numBuckets = mBuckets.length;
    433             mUnifiedLookupIndex = new int[mNumObjects + numBuckets];
    434             int currentUnifiedIndexEntry = 0;
    435             int nextUnifiedEntry;
    436 
    437             mMtpObjects = new MtpObjectInfo[mNumObjects];
    438             int currentItemsEntry = 0;
    439             for (int i = 0; i < numBuckets; i++) {
    440                 DateBucket bucket = mBuckets[i];
    441                 nextUnifiedEntry = currentUnifiedIndexEntry + bucket.tempElementsList.size() + 1;
    442                 Arrays.fill(mUnifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i);
    443                 bucket.unifiedStartIndex = currentUnifiedIndexEntry;
    444                 bucket.unifiedEndIndex = nextUnifiedEntry - 1;
    445                 currentUnifiedIndexEntry = nextUnifiedEntry;
    446 
    447                 bucket.itemsStartIndex = currentItemsEntry;
    448                 bucket.numItems = bucket.tempElementsList.size();
    449                 for (int j = 0; j < bucket.numItems; j++) {
    450                     mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j);
    451                     currentItemsEntry++;
    452                 }
    453                 bucket.tempElementsList = null;
    454             }
    455         }
    456 
    457         private void copyResults() {
    458             MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex;
    459             MtpDeviceIndex.this.mMtpObjects = mMtpObjects;
    460             MtpDeviceIndex.this.mBuckets = mBuckets;
    461             mUnifiedLookupIndex = null;
    462             mMtpObjects = null;
    463             mBuckets = null;
    464         }
    465 
    466         @Override
    467         public void run() {
    468             try {
    469                 indexDevice();
    470             } catch (IndexingException e) {
    471                 synchronized (MtpDeviceIndex.this) {
    472                     resetState();
    473                     if (mProgressListener != null) {
    474                         mProgressListener.onIndexFinish();
    475                     }
    476                 }
    477             }
    478         }
    479 
    480         private void indexDevice() throws IndexingException {
    481             synchronized (MtpDeviceIndex.this) {
    482                 mProgress = Progress.Started;
    483             }
    484             mBucketsTemp = new HashMap<SimpleDate, DateBucket>();
    485             for (int storageId : mDevice.getStorageIds()) {
    486                 if (mDevice != getDevice()) throw new IndexingException();
    487                 Stack<Integer> pendingDirectories = new Stack<Integer>();
    488                 pendingDirectories.add(0xFFFFFFFF); // start at the root of the device
    489                 while (!pendingDirectories.isEmpty()) {
    490                     if (mDevice != getDevice()) throw new IndexingException();
    491                     int dirHandle = pendingDirectories.pop();
    492                     for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) {
    493                         MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle);
    494                         if (objectInfo == null) throw new IndexingException();
    495                         switch (objectInfo.getFormat()) {
    496                             case MtpConstants.FORMAT_JFIF:
    497                             case MtpConstants.FORMAT_EXIF_JPEG:
    498                                 addObject(objectInfo);
    499                                 break;
    500                             case MtpConstants.FORMAT_ASSOCIATION:
    501                                 pendingDirectories.add(objectHandle);
    502                                 break;
    503                         }
    504                     }
    505                 }
    506             }
    507             Collection<DateBucket> values = mBucketsTemp.values();
    508             mBucketsTemp = null;
    509             mBuckets = values.toArray(new DateBucket[values.size()]);
    510             values = null;
    511             synchronized (MtpDeviceIndex.this) {
    512                 mProgress = Progress.Sorting;
    513                 if (mProgressListener != null) {
    514                     mProgressListener.onSorting();
    515                 }
    516             }
    517             sortAll();
    518             buildLookupIndex();
    519             synchronized (MtpDeviceIndex.this) {
    520                 if (mDevice != getDevice()) throw new IndexingException();
    521                 copyResults();
    522 
    523                 /*
    524                  * In order for getBuckets to operate in constant time for descending
    525                  * order, we must precompute a reversed array of the buckets, mainly
    526                  * because the android.widget.SectionIndexer interface which adapters
    527                  * that call getBuckets implement depends on section numbers to be
    528                  * ascending relative to the scroll position, so we must have this for
    529                  * descending order or the scrollbar goes crazy.
    530                  */
    531                 computeReversedBuckets();
    532 
    533                 mProgress = Progress.Finished;
    534                 if (mProgressListener != null) {
    535                     mProgressListener.onIndexFinish();
    536                 }
    537             }
    538         }
    539 
    540         private SimpleDate mDateInstance = new SimpleDate();
    541 
    542         private void addObject(MtpObjectInfo objectInfo) {
    543             mNumObjects++;
    544             mDateInstance.setTimestamp(objectInfo.getDateCreated());
    545             DateBucket bucket = mBucketsTemp.get(mDateInstance);
    546             if (bucket == null) {
    547                 bucket = new DateBucket(mDateInstance, objectInfo);
    548                 mBucketsTemp.put(mDateInstance, bucket);
    549                 mDateInstance = new SimpleDate(); // only create new date
    550                                                   // objects when they are used
    551                 return;
    552             } else {
    553                 bucket.tempElementsList.add(objectInfo);
    554             }
    555             if (mProgressListener != null) {
    556                 mProgressListener.onObjectIndexed(objectInfo, mNumObjects);
    557             }
    558         }
    559 
    560         private void sortAll() {
    561             Arrays.sort(mBuckets);
    562             for (DateBucket bucket : mBuckets) {
    563                 bucket.sortElements(sMtpObjectComparator);
    564             }
    565         }
    566 
    567     }
    568 
    569     private void computeReversedBuckets() {
    570         mCachedReverseBuckets = new Object[mBuckets.length];
    571         for (int i = 0; i < mCachedReverseBuckets.length; i++) {
    572             mCachedReverseBuckets[i] = mBuckets[mBuckets.length - 1 - i];
    573         }
    574     }
    575 }
    576