am 365768fd: Merge "Return EXIF thumbnails when available." into klp-dev

* commit '365768fd3533343d6631875d7d46882907f7ab09':
  Return EXIF thumbnails when available.
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 0d1a740..65c9220 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -16,6 +16,9 @@
 
 package android.provider;
 
+import static android.net.TrafficStats.KB_IN_BYTES;
+import static libcore.io.OsConstants.SEEK_SET;
+
 import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -36,7 +39,10 @@
 
 import com.google.android.collect.Lists;
 
+import libcore.io.ErrnoException;
+import libcore.io.IoBridge;
 import libcore.io.IoUtils;
+import libcore.io.Libcore;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -527,25 +533,53 @@
      * @return decoded thumbnail, or {@code null} if problem was encountered.
      */
     public static Bitmap getThumbnail(ContentResolver resolver, Uri documentUri, Point size) {
-        final Bundle opts = new Bundle();
-        opts.putParcelable(EXTRA_THUMBNAIL_SIZE, size);
+        final Bundle openOpts = new Bundle();
+        openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size);
 
         AssetFileDescriptor afd = null;
         try {
-            afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", opts);
+            afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts);
 
             final FileDescriptor fd = afd.getFileDescriptor();
-            final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options();
+            final long offset = afd.getStartOffset();
+            final long length = afd.getDeclaredLength();
 
-            bitmapOpts.inJustDecodeBounds = true;
-            BitmapFactory.decodeFileDescriptor(fd, null, bitmapOpts);
+            // Some thumbnails might be a region inside a larger file, such as
+            // an EXIF thumbnail. Since BitmapFactory aggressively seeks around
+            // the entire file, we read the region manually.
+            byte[] region = null;
+            if (offset > 0 && length <= 64 * KB_IN_BYTES) {
+                region = new byte[(int) length];
+                Libcore.os.lseek(fd, offset, SEEK_SET);
+                if (IoBridge.read(fd, region, 0, region.length) != region.length) {
+                    region = null;
+                }
+            }
 
-            final int widthSample = bitmapOpts.outWidth / size.x;
-            final int heightSample = bitmapOpts.outHeight / size.y;
+            // We requested a rough thumbnail size, but the remote size may have
+            // returned something giant, so defensively scale down as needed.
+            final BitmapFactory.Options opts = new BitmapFactory.Options();
+            opts.inJustDecodeBounds = true;
+            if (region != null) {
+                BitmapFactory.decodeByteArray(region, 0, region.length, opts);
+            } else {
+                BitmapFactory.decodeFileDescriptor(fd, null, opts);
+            }
 
-            bitmapOpts.inJustDecodeBounds = false;
-            bitmapOpts.inSampleSize = Math.min(widthSample, heightSample);
-            return BitmapFactory.decodeFileDescriptor(fd, null, bitmapOpts);
+            final int widthSample = opts.outWidth / size.x;
+            final int heightSample = opts.outHeight / size.y;
+
+            opts.inJustDecodeBounds = false;
+            opts.inSampleSize = Math.min(widthSample, heightSample);
+            Log.d(TAG, "Decoding with sample size " + opts.inSampleSize);
+            if (region != null) {
+                return BitmapFactory.decodeByteArray(region, 0, region.length, opts);
+            } else {
+                return BitmapFactory.decodeFileDescriptor(fd, null, opts);
+            }
+        } catch (ErrnoException e) {
+            Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
+            return null;
         } catch (IOException e) {
             Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
             return null;
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 4cd3e37..20eb356 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -291,6 +291,20 @@
     }
 
     /**
+     * Returns the offset and length of thumbnail inside the JPEG file, or
+     * {@code null} if there is no thumbnail.
+     *
+     * @return two-element array, the offset in the first value, and length in
+     *         the second, or {@code null} if no thumbnail was found.
+     * @hide
+     */
+    public long[] getThumbnailRange() {
+        synchronized (sLock) {
+            return getThumbnailRangeNative(mFilename);
+        }
+    }
+
+    /**
      * Stores the latitude and longitude value in a float array. The first element is
      * the latitude, and the second element is the longitude. Returns false if the
      * Exif tags are not available.
@@ -416,4 +430,6 @@
     private native void commitChangesNative(String fileName);
 
     private native byte[] getThumbnailNative(String fileName);
+
+    private native long[] getThumbnailRangeNative(String fileName);
 }
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index b4bf563..8843e19 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -20,10 +20,13 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.UriMatcher;
+import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
+import android.media.ExifInterface;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.provider.DocumentsContract;
@@ -296,7 +299,6 @@
                 final Root root = mRoots.get(DocumentsContract.getRootId(uri));
                 final String docId = DocumentsContract.getDocId(uri);
 
-                // TODO: offer as thumbnail
                 final File file = docIdToFile(root, docId);
                 return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode));
             }
@@ -307,6 +309,39 @@
     }
 
     @Override
+    public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
+            throws FileNotFoundException {
+        if (opts == null || !opts.containsKey(DocumentsContract.EXTRA_THUMBNAIL_SIZE)) {
+            return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
+        }
+
+        switch (sMatcher.match(uri)) {
+            case URI_DOCS_ID: {
+                final Root root = mRoots.get(DocumentsContract.getRootId(uri));
+                final String docId = DocumentsContract.getDocId(uri);
+
+                final File file = docIdToFile(root, docId);
+                final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
+                        file, ParcelFileDescriptor.MODE_READ_ONLY);
+
+                try {
+                    final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
+                    final long[] thumb = exif.getThumbnailRange();
+                    if (thumb != null) {
+                        return new AssetFileDescriptor(pfd, thumb[0], thumb[1]);
+                    }
+                } catch (IOException e) {
+                }
+
+                return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
+            }
+            default: {
+                throw new UnsupportedOperationException("Unsupported Uri " + uri);
+            }
+        }
+    }
+
+    @Override
     public Uri insert(Uri uri, ContentValues values) {
         switch (sMatcher.match(uri)) {
             case URI_DOCS_ID: {