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.HashSet; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Set; 33 import java.util.Stack; 34 35 /** 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. 38 * 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. 41 * 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] 46 * 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] 49 * 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] 54 * 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 61 * - isFirstInBucket 62 * 63 * See the comments in buildLookupIndex for implementation notes. 64 */ 65 public class MtpDeviceIndex { 66 67 public static final int FORMAT_MOV = 0x300D; // For some reason this is not in MtpConstants 68 69 public static final Set<Integer> SUPPORTED_IMAGE_FORMATS; 70 public static final Set<Integer> SUPPORTED_VIDEO_FORMATS; 71 72 static { 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); 79 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 86 } 87 88 @Override 89 public int hashCode() { 90 final int prime = 31; 91 int result = 1; 92 result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId()); 93 result = prime * result + mGeneration; 94 return result; 95 } 96 97 public interface ProgressListener { 98 public void onObjectIndexed(MtpObjectInfo object, int numVisited); 99 100 public void onSorting(); 101 102 public void onIndexFinish(); 103 } 104 105 public enum SortOrder { 106 Ascending, Descending 107 } 108 109 private MtpDevice mDevice; 110 private int[] mUnifiedLookupIndex; 111 private MtpObjectInfo[] mMtpObjects; 112 private DateBucket[] mBuckets; 113 private int mGeneration = 0; 114 115 public enum Progress { 116 Uninitialized, Initialized, Pending, Started, Sorting, Finished 117 } 118 119 private Progress mProgress = Progress.Uninitialized; 120 private ProgressListener mProgressListener; 121 122 private static final MtpDeviceIndex sInstance = new MtpDeviceIndex(); 123 private static final MtpObjectTimestampComparator sMtpObjectComparator = 124 new MtpObjectTimestampComparator(); 125 126 public static MtpDeviceIndex getInstance() { 127 return sInstance; 128 } 129 130 private MtpDeviceIndex() { 131 } 132 133 synchronized public MtpDevice getDevice() { 134 return mDevice; 135 } 136 137 /** 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()} 141 * 142 * @param device The MtpDevice that should be indexed 143 */ 144 synchronized public void setDevice(MtpDevice device) { 145 if (device == mDevice) return; 146 mDevice = device; 147 resetState(); 148 } 149 150 /** 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. 154 * 155 * @return Runnable for the main indexing task 156 */ 157 synchronized public Runnable getIndexRunnable() { 158 if (mProgress != Progress.Initialized) return null; 159 mProgress = Progress.Pending; 160 return new IndexRunnable(mDevice); 161 } 162 163 synchronized public boolean indexReady() { 164 return mProgress == Progress.Finished; 165 } 166 167 synchronized public Progress getProgress() { 168 return mProgress; 169 } 170 171 /** 172 * @param listener Listener to change to 173 * @return Progress at the time the listener was added (useful for 174 * configuring initial UI state) 175 */ 176 synchronized public Progress setProgressListener(ProgressListener listener) { 177 mProgressListener = listener; 178 return mProgress; 179 } 180 181 /** 182 * Make the listener null if it matches the argument 183 * 184 * @param listener Listener to unset, if currently registered 185 */ 186 synchronized public void unsetProgressListener(ProgressListener listener) { 187 if (mProgressListener == listener) 188 mProgressListener = null; 189 } 190 191 /** 192 * @return The total number of elements in the index (labels and items) 193 */ 194 public int size() { 195 return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0; 196 } 197 198 /** 199 * @param position Index of item to fetch, where 0 is the first item in the 200 * specified order 201 * @param order 202 * @return the bucket label or MtpObjectInfo at the specified position and 203 * order 204 */ 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; 211 } else { 212 return mMtpObjects[bucket.itemsStartIndex + position - 1 213 - bucket.unifiedStartIndex]; 214 } 215 } else { 216 int zeroIndex = mUnifiedLookupIndex.length - 1 - position; 217 DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]]; 218 if (bucket.unifiedEndIndex == zeroIndex) { 219 return bucket.bucket; 220 } else { 221 return mMtpObjects[bucket.itemsStartIndex + zeroIndex 222 - bucket.unifiedStartIndex]; 223 } 224 } 225 } 226 227 /** 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 231 */ 232 public MtpObjectInfo getWithoutLabels(int position, SortOrder order) { 233 if (mProgress != Progress.Finished) return null; 234 if (order == SortOrder.Ascending) { 235 return mMtpObjects[position]; 236 } else { 237 return mMtpObjects[mMtpObjects.length - 1 - position]; 238 } 239 } 240 241 /** 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 248 * @param order 249 * @return position in a view of the data that does include labels 250 */ 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; 255 } 256 int bucketNumber = 0; 257 int iMin = 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) { 262 iMin = iMid + 1; 263 } else if (mBuckets[iMid].itemsStartIndex > position) { 264 iMax = iMid - 1; 265 } else { 266 bucketNumber = iMid; 267 break; 268 } 269 } 270 if (mBuckets.length == 0 || mUnifiedLookupIndex.length == 0) { 271 return -1; 272 } 273 int mappedPos = mBuckets[bucketNumber].unifiedStartIndex 274 + position - mBuckets[bucketNumber].itemsStartIndex; 275 if (order == SortOrder.Descending) { 276 mappedPos = mUnifiedLookupIndex.length - 1 - mappedPos; 277 } 278 return mappedPos; 279 } 280 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; 287 } else { 288 int zeroIndex = mUnifiedLookupIndex.length - 1 - position; 289 if (mBuckets.length == 0 || mUnifiedLookupIndex.length == 0) { 290 return -1; 291 } 292 DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]]; 293 if (bucket.unifiedEndIndex == zeroIndex) zeroIndex--; 294 return mMtpObjects.length - 1 - bucket.itemsStartIndex 295 - zeroIndex + bucket.unifiedStartIndex; 296 } 297 } 298 299 /** 300 * @return The number of MTP items in the index (without labels) 301 */ 302 public int sizeWithoutLabels() { 303 return mProgress == Progress.Finished ? mMtpObjects.length : 0; 304 } 305 306 public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) { 307 if (order == SortOrder.Ascending) { 308 return mBuckets[bucketNumber].unifiedStartIndex; 309 } else { 310 return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1; 311 } 312 } 313 314 public int getBucketNumberForPosition(int position, SortOrder order) { 315 if (order == SortOrder.Ascending) { 316 return mUnifiedLookupIndex[position]; 317 } else { 318 return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position]; 319 } 320 } 321 322 public boolean isFirstInBucket(int position, SortOrder order) { 323 if (order == SortOrder.Ascending) { 324 return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position; 325 } else { 326 position = mUnifiedLookupIndex.length - 1 - position; 327 return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position; 328 } 329 } 330 331 private Object[] mCachedReverseBuckets; 332 333 public Object[] getBuckets(SortOrder order) { 334 if (mBuckets == null) return null; 335 if (order == SortOrder.Ascending) { 336 return mBuckets; 337 } else { 338 if (mCachedReverseBuckets == null) { 339 computeReversedBuckets(); 340 } 341 return mCachedReverseBuckets; 342 } 343 } 344 345 /* 346 * See the comments for buildLookupIndex for notes on the specific fields of 347 * this class. 348 */ 349 private class DateBucket implements Comparable<DateBucket> { 350 SimpleDate bucket; 351 List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>(); 352 int unifiedStartIndex; 353 int unifiedEndIndex; 354 int itemsStartIndex; 355 int numItems; 356 357 public DateBucket(SimpleDate bucket) { 358 this.bucket = bucket; 359 } 360 361 public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) { 362 this(bucket); 363 tempElementsList.add(firstElement); 364 } 365 366 void sortElements(Comparator<MtpObjectInfo> comparator) { 367 Collections.sort(tempElementsList, comparator); 368 } 369 370 @Override 371 public String toString() { 372 return bucket.toString(); 373 } 374 375 @Override 376 public int hashCode() { 377 return bucket.hashCode(); 378 } 379 380 @Override 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)) { 389 return false; 390 } 391 return true; 392 } 393 394 @Override 395 public int compareTo(DateBucket another) { 396 return this.bucket.compareTo(another.bucket); 397 } 398 } 399 400 /** 401 * Comparator to sort MtpObjectInfo objects by date created. 402 */ 403 private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> { 404 @Override 405 public int compare(MtpObjectInfo o1, MtpObjectInfo o2) { 406 long diff = o1.getDateCreated() - o2.getDateCreated(); 407 if (diff < 0) { 408 return -1; 409 } else if (diff == 0) { 410 return 0; 411 } else { 412 return 1; 413 } 414 } 415 } 416 417 private void resetState() { 418 mGeneration++; 419 mUnifiedLookupIndex = null; 420 mMtpObjects = null; 421 mBuckets = null; 422 mCachedReverseBuckets = null; 423 mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized; 424 } 425 426 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; 434 435 private class IndexingException extends Exception {}; 436 437 public IndexRunnable(MtpDevice device) { 438 mDevice = device; 439 } 440 441 /* 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 445 * orders. 446 * 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] 450 * 451 * In this case, the lookup index array would be 452 * [0, 0, 0, 1, 1] 453 * 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. 459 */ 460 private void buildLookupIndex() { 461 int numBuckets = mBuckets.length; 462 mUnifiedLookupIndex = new int[mNumObjects + numBuckets]; 463 int currentUnifiedIndexEntry = 0; 464 int nextUnifiedEntry; 465 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; 475 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); 480 currentItemsEntry++; 481 } 482 bucket.tempElementsList = null; 483 } 484 } 485 486 private void copyResults() { 487 MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex; 488 MtpDeviceIndex.this.mMtpObjects = mMtpObjects; 489 MtpDeviceIndex.this.mBuckets = mBuckets; 490 mUnifiedLookupIndex = null; 491 mMtpObjects = null; 492 mBuckets = null; 493 } 494 495 @Override 496 public void run() { 497 try { 498 indexDevice(); 499 } catch (IndexingException e) { 500 synchronized (MtpDeviceIndex.this) { 501 resetState(); 502 if (mProgressListener != null) { 503 mProgressListener.onIndexFinish(); 504 } 505 } 506 } 507 } 508 509 private void indexDevice() throws IndexingException { 510 synchronized (MtpDeviceIndex.this) { 511 mProgress = Progress.Started; 512 } 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); 530 } 531 } 532 } 533 } 534 Collection<DateBucket> values = mBucketsTemp.values(); 535 mBucketsTemp = null; 536 mBuckets = values.toArray(new DateBucket[values.size()]); 537 values = null; 538 synchronized (MtpDeviceIndex.this) { 539 mProgress = Progress.Sorting; 540 if (mProgressListener != null) { 541 mProgressListener.onSorting(); 542 } 543 } 544 sortAll(); 545 buildLookupIndex(); 546 synchronized (MtpDeviceIndex.this) { 547 if (mDevice != getDevice()) throw new IndexingException(); 548 copyResults(); 549 550 /* 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. 557 */ 558 computeReversedBuckets(); 559 560 mProgress = Progress.Finished; 561 if (mProgressListener != null) { 562 mProgressListener.onIndexFinish(); 563 } 564 } 565 } 566 567 private SimpleDate mDateInstance = new SimpleDate(); 568 569 private void addObject(MtpObjectInfo objectInfo) { 570 mNumObjects++; 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 578 return; 579 } else { 580 bucket.tempElementsList.add(objectInfo); 581 } 582 if (mProgressListener != null) { 583 mProgressListener.onObjectIndexed(objectInfo, mNumObjects); 584 } 585 } 586 587 private void sortAll() { 588 Arrays.sort(mBuckets); 589 for (DateBucket bucket : mBuckets) { 590 bucket.sortElements(sMtpObjectComparator); 591 } 592 } 593 594 } 595 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]; 600 } 601 } 602 } 603