OSDN Git Service

Importer: Full-screen viewing, UI refinement, refactoring
[android-x86/packages-apps-Gallery2.git] / src / com / android / gallery3d / ingest / MtpDeviceIndex.java
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 }