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