1 package com.android.gallery3d.ingest.data; 2 3 import android.annotation.TargetApi; 4 import android.mtp.MtpConstants; 5 import android.mtp.MtpDevice; 6 import android.mtp.MtpObjectInfo; 7 import android.os.Build; 8 import android.webkit.MimeTypeMap; 9 10 import java.util.Collections; 11 import java.util.HashMap; 12 import java.util.HashSet; 13 import java.util.Locale; 14 import java.util.Map; 15 import java.util.Set; 16 17 /** 18 * Index of MTP media objects organized into "buckets," or groupings, based on the date 19 * they were created. 20 * 21 * When the index is created, the buckets are sorted in their natural 22 * order, and the items within the buckets sorted by the date they are taken. 23 * 24 * The index enables the access of items and bucket labels as one unified list. 25 * For example, let's say we have the following data in the index: 26 * [Bucket A]: [photo 1], [photo 2] 27 * [Bucket B]: [photo 3] 28 * 29 * Then the items can be thought of as being organized as a 5 element list: 30 * [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3] 31 * 32 * The data can also be accessed in descending order, in which case the list 33 * would be a bit different from simply reversing the ascending list, since the 34 * bucket labels need to always be at the beginning: 35 * [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1] 36 * 37 * The index enables all the following operations in constant time, both for 38 * ascending and descending views of the data: 39 * - get/getAscending/getDescending: get an item at a specified list position 40 * - size: get the total number of items (bucket labels and MTP objects) 41 * - getFirstPositionForBucketNumber 42 * - getBucketNumberForPosition 43 * - isFirstInBucket 44 * 45 * See {@link MtpDeviceIndexRunnable} for implementation notes. 46 */ 47 @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) 48 public class MtpDeviceIndex { 49 50 /** 51 * Indexing progress listener. 52 */ 53 public interface ProgressListener { 54 /** 55 * A media item on the device was indexed. 56 * @param object The media item that was just indexed 57 * @param numVisited Number of items visited so far 58 */ onObjectIndexed(IngestObjectInfo object, int numVisited)59 public void onObjectIndexed(IngestObjectInfo object, int numVisited); 60 61 /** 62 * The metadata loaded from the device is being sorted. 63 */ onSortingStarted()64 public void onSortingStarted(); 65 66 /** 67 * The indexing is done and the index is ready to be used. 68 */ onIndexingFinished()69 public void onIndexingFinished(); 70 } 71 72 /** 73 * Media sort orders. 74 */ 75 public enum SortOrder { 76 ASCENDING, DESCENDING 77 } 78 79 /** Quicktime MOV container (not already defined in {@link MtpConstants}) **/ 80 public static final int FORMAT_MOV = 0x300D; 81 82 public static final Set<Integer> SUPPORTED_IMAGE_FORMATS; 83 public static final Set<Integer> SUPPORTED_VIDEO_FORMATS; 84 85 static { 86 Set<Integer> supportedImageFormats = new HashSet<Integer>(); 87 supportedImageFormats.add(MtpConstants.FORMAT_JFIF); 88 supportedImageFormats.add(MtpConstants.FORMAT_EXIF_JPEG); 89 supportedImageFormats.add(MtpConstants.FORMAT_PNG); 90 supportedImageFormats.add(MtpConstants.FORMAT_GIF); 91 supportedImageFormats.add(MtpConstants.FORMAT_BMP); 92 supportedImageFormats.add(MtpConstants.FORMAT_TIFF); 93 supportedImageFormats.add(MtpConstants.FORMAT_TIFF_EP); 94 if (Build.VERSION.SDK_INT >= 24) { 95 supportedImageFormats.add(MtpConstants.FORMAT_DNG); 96 } 97 SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableSet(supportedImageFormats); 98 99 Set<Integer> supportedVideoFormats = new HashSet<Integer>(); 100 supportedVideoFormats.add(MtpConstants.FORMAT_3GP_CONTAINER); 101 supportedVideoFormats.add(MtpConstants.FORMAT_AVI); 102 supportedVideoFormats.add(MtpConstants.FORMAT_MP4_CONTAINER); 103 supportedVideoFormats.add(MtpConstants.FORMAT_MP2); 104 supportedVideoFormats.add(MtpConstants.FORMAT_MPEG); 105 // TODO(georgescu): add FORMAT_MOV once Android Media Scanner supports .mov files 106 SUPPORTED_VIDEO_FORMATS = Collections.unmodifiableSet(supportedVideoFormats); 107 } 108 109 private MtpDevice mDevice; 110 private long mGeneration; 111 private ProgressListener mProgressListener; 112 private volatile MtpDeviceIndexRunnable.Results mResults; 113 private final MtpDeviceIndexRunnable.Factory mIndexRunnableFactory; 114 115 private static final MtpDeviceIndex sInstance = new MtpDeviceIndex( 116 MtpDeviceIndexRunnable.getFactory()); 117 118 private static final Map<String, Boolean> sCachedSupportedExtenstions = new HashMap<>(); 119 getInstance()120 public static MtpDeviceIndex getInstance() { 121 return sInstance; 122 } 123 MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory)124 protected MtpDeviceIndex(MtpDeviceIndexRunnable.Factory indexRunnableFactory) { 125 mIndexRunnableFactory = indexRunnableFactory; 126 } 127 getDevice()128 public synchronized MtpDevice getDevice() { 129 return mDevice; 130 } 131 isDeviceConnected()132 public synchronized boolean isDeviceConnected() { 133 return (mDevice != null); 134 } 135 136 /** 137 * @param mtpObjectInfo MTP object info 138 * @return Whether the format is supported by this index. 139 */ isFormatSupported(MtpObjectInfo mtpObjectInfo)140 public boolean isFormatSupported(MtpObjectInfo mtpObjectInfo) { 141 // Checks whether the format is supported or not. 142 final int format = mtpObjectInfo.getFormat(); 143 if (SUPPORTED_IMAGE_FORMATS.contains(format) 144 || SUPPORTED_VIDEO_FORMATS.contains(format)) { 145 return true; 146 } 147 148 // Checks whether the extension is supported or not. 149 final String name = mtpObjectInfo.getName(); 150 if (name == null) { 151 return false; 152 } 153 final int lastDot = name.lastIndexOf('.'); 154 if (lastDot >= 0) { 155 final String extension = name.substring(lastDot + 1); 156 157 Boolean result = sCachedSupportedExtenstions.get(extension); 158 if (result != null) { 159 return result; 160 } 161 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 162 extension.toLowerCase(Locale.US)); 163 if (mime != null) { 164 // This will also accept the newly added mimetypes for images and videos. 165 result = mime.startsWith("image/") || mime.startsWith("video/"); 166 sCachedSupportedExtenstions.put(extension, result); 167 return result; 168 } 169 } 170 171 return false; 172 } 173 174 /** 175 * Sets the MtpDevice that should be indexed and initializes state, but does 176 * not kick off the actual indexing task, which is instead done by using 177 * {@link #getIndexRunnable()} 178 * 179 * @param device The MtpDevice that should be indexed 180 */ setDevice(MtpDevice device)181 public synchronized void setDevice(MtpDevice device) { 182 if (device == mDevice) { 183 return; 184 } 185 mDevice = device; 186 resetState(); 187 } 188 189 /** 190 * Provides a Runnable for the indexing task (assuming the state has already 191 * been correctly initialized by calling {@link #setDevice(MtpDevice)}). 192 * 193 * @return Runnable for the main indexing task 194 */ getIndexRunnable()195 public synchronized Runnable getIndexRunnable() { 196 if (!isDeviceConnected() || mResults != null) { 197 return null; 198 } 199 return mIndexRunnableFactory.createMtpDeviceIndexRunnable(this); 200 } 201 202 /** 203 * @return Whether the index is ready to be used. 204 */ isIndexReady()205 public synchronized boolean isIndexReady() { 206 return mResults != null; 207 } 208 209 /** 210 * @param listener 211 * @return Current progress (useful for configuring initial UI state) 212 */ setProgressListener(ProgressListener listener)213 public synchronized void setProgressListener(ProgressListener listener) { 214 mProgressListener = listener; 215 } 216 217 /** 218 * Make the listener null if it matches the argument 219 * 220 * @param listener Listener to unset, if currently registered 221 */ unsetProgressListener(ProgressListener listener)222 public synchronized void unsetProgressListener(ProgressListener listener) { 223 if (mProgressListener == listener) { 224 mProgressListener = null; 225 } 226 } 227 228 /** 229 * @return The total number of elements in the index (labels and items) 230 */ size()231 public int size() { 232 MtpDeviceIndexRunnable.Results results = mResults; 233 return results != null ? results.unifiedLookupIndex.length : 0; 234 } 235 236 /** 237 * @param position Index of item to fetch, where 0 is the first item in the 238 * specified order 239 * @param order 240 * @return the bucket label or IngestObjectInfo at the specified position and 241 * order 242 */ get(int position, SortOrder order)243 public Object get(int position, SortOrder order) { 244 MtpDeviceIndexRunnable.Results results = mResults; 245 if (results == null) { 246 return null; 247 } 248 if (order == SortOrder.ASCENDING) { 249 DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]]; 250 if (bucket.unifiedStartIndex == position) { 251 return bucket.date; 252 } else { 253 return results.mtpObjects[bucket.itemsStartIndex + position - 1 254 - bucket.unifiedStartIndex]; 255 } 256 } else { 257 int zeroIndex = results.unifiedLookupIndex.length - 1 - position; 258 DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]]; 259 if (bucket.unifiedEndIndex == zeroIndex) { 260 return bucket.date; 261 } else { 262 return results.mtpObjects[bucket.itemsStartIndex + zeroIndex 263 - bucket.unifiedStartIndex]; 264 } 265 } 266 } 267 268 /** 269 * @param position Index of item to fetch from a view of the data that does not 270 * include labels and is in the specified order 271 * @return position-th item in specified order, when not including labels 272 */ getWithoutLabels(int position, SortOrder order)273 public IngestObjectInfo getWithoutLabels(int position, SortOrder order) { 274 MtpDeviceIndexRunnable.Results results = mResults; 275 if (results == null) { 276 return null; 277 } 278 if (order == SortOrder.ASCENDING) { 279 return results.mtpObjects[position]; 280 } else { 281 return results.mtpObjects[results.mtpObjects.length - 1 - position]; 282 } 283 } 284 285 /** 286 * @param position Index of item to map from a view of the data that does not 287 * include labels and is in the specified order 288 * @param order 289 * @return position in a view of the data that does include labels, or -1 if the index isn't 290 * ready 291 */ getPositionFromPositionWithoutLabels(int position, SortOrder order)292 public int getPositionFromPositionWithoutLabels(int position, SortOrder order) { 293 /* Although this is O(log(number of buckets)), and thus should not be used 294 in hotspots, even if the attached device has items for every day for 295 a five-year timeframe, it would still only take 11 iterations at most, 296 so shouldn't be a huge issue. */ 297 MtpDeviceIndexRunnable.Results results = mResults; 298 if (results == null) { 299 return -1; 300 } 301 if (order == SortOrder.DESCENDING) { 302 position = results.mtpObjects.length - 1 - position; 303 } 304 int bucketNumber = 0; 305 int iMin = 0; 306 int iMax = results.buckets.length - 1; 307 while (iMax >= iMin) { 308 int iMid = (iMax + iMin) / 2; 309 if (results.buckets[iMid].itemsStartIndex + results.buckets[iMid].numItems 310 <= position) { 311 iMin = iMid + 1; 312 } else if (results.buckets[iMid].itemsStartIndex > position) { 313 iMax = iMid - 1; 314 } else { 315 bucketNumber = iMid; 316 break; 317 } 318 } 319 int mappedPos = results.buckets[bucketNumber].unifiedStartIndex + position 320 - results.buckets[bucketNumber].itemsStartIndex + 1; 321 if (order == SortOrder.DESCENDING) { 322 mappedPos = results.unifiedLookupIndex.length - mappedPos; 323 } 324 return mappedPos; 325 } 326 327 /** 328 * @param position Index of item to map from a view of the data that 329 * includes labels and is in the specified order 330 * @param order 331 * @return position in a view of the data that does not include labels, or -1 if the index isn't 332 * ready 333 */ getPositionWithoutLabelsFromPosition(int position, SortOrder order)334 public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) { 335 MtpDeviceIndexRunnable.Results results = mResults; 336 if (results == null) { 337 return -1; 338 } 339 if (order == SortOrder.ASCENDING) { 340 DateBucket bucket = results.buckets[results.unifiedLookupIndex[position]]; 341 if (bucket.unifiedStartIndex == position) { 342 position++; 343 } 344 return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex; 345 } else { 346 int zeroIndex = results.unifiedLookupIndex.length - 1 - position; 347 DateBucket bucket = results.buckets[results.unifiedLookupIndex[zeroIndex]]; 348 if (bucket.unifiedEndIndex == zeroIndex) { 349 zeroIndex--; 350 } 351 return results.mtpObjects.length - 1 - bucket.itemsStartIndex 352 - zeroIndex + bucket.unifiedStartIndex; 353 } 354 } 355 356 /** 357 * @return The number of media items in the index 358 */ sizeWithoutLabels()359 public int sizeWithoutLabels() { 360 MtpDeviceIndexRunnable.Results results = mResults; 361 return results != null ? results.mtpObjects.length : 0; 362 } 363 364 /** 365 * @param bucketNumber Index of bucket in the specified order 366 * @param order 367 * @return position of bucket's first item in a view of the data that includes labels 368 */ getFirstPositionForBucketNumber(int bucketNumber, SortOrder order)369 public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) { 370 MtpDeviceIndexRunnable.Results results = mResults; 371 if (order == SortOrder.ASCENDING) { 372 return results.buckets[bucketNumber].unifiedStartIndex; 373 } else { 374 return results.unifiedLookupIndex.length 375 - results.buckets[results.buckets.length - 1 - bucketNumber].unifiedEndIndex 376 - 1; 377 } 378 } 379 380 /** 381 * @param position Index of item in the view of the data that includes labels and is in 382 * the specified order 383 * @param order 384 * @return Index of the bucket that contains the specified item 385 */ getBucketNumberForPosition(int position, SortOrder order)386 public int getBucketNumberForPosition(int position, SortOrder order) { 387 MtpDeviceIndexRunnable.Results results = mResults; 388 if (order == SortOrder.ASCENDING) { 389 return results.unifiedLookupIndex[position]; 390 } else { 391 return results.buckets.length - 1 392 - results.unifiedLookupIndex[results.unifiedLookupIndex.length - 1 393 - position]; 394 } 395 } 396 397 /** 398 * @param position Index of item in the view of the data that includes labels and is in 399 * the specified order 400 * @param order 401 * @return Whether the specified item is the first item in its bucket 402 */ isFirstInBucket(int position, SortOrder order)403 public boolean isFirstInBucket(int position, SortOrder order) { 404 MtpDeviceIndexRunnable.Results results = mResults; 405 if (order == SortOrder.ASCENDING) { 406 return results.buckets[results.unifiedLookupIndex[position]].unifiedStartIndex 407 == position; 408 } else { 409 position = results.unifiedLookupIndex.length - 1 - position; 410 return results.buckets[results.unifiedLookupIndex[position]].unifiedEndIndex 411 == position; 412 } 413 } 414 415 /** 416 * @param order 417 * @return Array of buckets in the specified order 418 */ getBuckets(SortOrder order)419 public DateBucket[] getBuckets(SortOrder order) { 420 MtpDeviceIndexRunnable.Results results = mResults; 421 if (results == null) { 422 return null; 423 } 424 return (order == SortOrder.ASCENDING) ? results.buckets : results.reversedBuckets; 425 } 426 resetState()427 protected void resetState() { 428 mGeneration++; 429 mResults = null; 430 } 431 432 /** 433 * @param device 434 * @param generation 435 * @return whether the index is at the given generation and the given device is connected 436 */ isAtGeneration(MtpDevice device, long generation)437 protected boolean isAtGeneration(MtpDevice device, long generation) { 438 return (mGeneration == generation) && (mDevice == device); 439 } 440 setIndexingResults(MtpDevice device, long generation, MtpDeviceIndexRunnable.Results results)441 protected synchronized boolean setIndexingResults(MtpDevice device, long generation, 442 MtpDeviceIndexRunnable.Results results) { 443 if (!isAtGeneration(device, generation)) { 444 return false; 445 } 446 mResults = results; 447 onIndexFinish(true /*successful*/); 448 return true; 449 } 450 onIndexFinish(boolean successful)451 protected synchronized void onIndexFinish(boolean successful) { 452 if (!successful) { 453 resetState(); 454 } 455 if (mProgressListener != null) { 456 mProgressListener.onIndexingFinished(); 457 } 458 } 459 onSorting()460 protected synchronized void onSorting() { 461 if (mProgressListener != null) { 462 mProgressListener.onSortingStarted(); 463 } 464 } 465 onObjectIndexed(IngestObjectInfo object, int numVisited)466 protected synchronized void onObjectIndexed(IngestObjectInfo object, int numVisited) { 467 if (mProgressListener != null) { 468 mProgressListener.onObjectIndexed(object, numVisited); 469 } 470 } 471 getGeneration()472 protected long getGeneration() { 473 return mGeneration; 474 } 475 } 476