Add extras to AFD, send orientation metadata.

AssetFileDescriptor augments a ParcelFileDescriptor with details
about how it should be interpreted, so extend it to support a Bundle
of extras.  Then use these extras to share thumbnail orientation
metadata.

The raw image data of EXIF thumbnails matches the orientation of
the enclosing image, but the thumbnail data doesn't repeat the EXIF
flags.  This meant that receivers of openDocumentThumbnail() would
get an image that needed to be transformed, but without enough
context to actually transform it.

Instead of transforming and recompressing the image on the fly on
the provider side, send a transformation hint that the receiver
side can interpret.

Bug: 11205688
Change-Id: Ibc5a7ad002377a55e6ffcb5ac5c8829841002e06
diff --git a/api/current.txt b/api/current.txt
index 40c7bf5..abb330e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -7497,11 +7497,13 @@
 
   public class AssetFileDescriptor implements java.io.Closeable android.os.Parcelable {
     ctor public AssetFileDescriptor(android.os.ParcelFileDescriptor, long, long);
+    ctor public AssetFileDescriptor(android.os.ParcelFileDescriptor, long, long, android.os.Bundle);
     method public void close() throws java.io.IOException;
     method public java.io.FileInputStream createInputStream() throws java.io.IOException;
     method public java.io.FileOutputStream createOutputStream() throws java.io.IOException;
     method public int describeContents();
     method public long getDeclaredLength();
+    method public android.os.Bundle getExtras();
     method public java.io.FileDescriptor getFileDescriptor();
     method public long getLength();
     method public android.os.ParcelFileDescriptor getParcelFileDescriptor();
diff --git a/core/java/android/content/res/AssetFileDescriptor.java b/core/java/android/content/res/AssetFileDescriptor.java
index e4cc77f..28edde0 100644
--- a/core/java/android/content/res/AssetFileDescriptor.java
+++ b/core/java/android/content/res/AssetFileDescriptor.java
@@ -16,6 +16,7 @@
 
 package android.content.res;
 
+import android.os.Bundle;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
@@ -42,17 +43,35 @@
     private final ParcelFileDescriptor mFd;
     private final long mStartOffset;
     private final long mLength;
-    
+    private final Bundle mExtras;
+
     /**
      * Create a new AssetFileDescriptor from the given values.
+     *
      * @param fd The underlying file descriptor.
      * @param startOffset The location within the file that the asset starts.
-     * This must be 0 if length is UNKNOWN_LENGTH.
+     *            This must be 0 if length is UNKNOWN_LENGTH.
      * @param length The number of bytes of the asset, or
-     * {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
+     *            {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
      */
     public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset,
             long length) {
+        this(fd, startOffset, length, null);
+    }
+
+    /**
+     * Create a new AssetFileDescriptor from the given values.
+     *
+     * @param fd The underlying file descriptor.
+     * @param startOffset The location within the file that the asset starts.
+     *            This must be 0 if length is UNKNOWN_LENGTH.
+     * @param length The number of bytes of the asset, or
+     *            {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
+     * @param extras additional details that can be used to interpret the
+     *            underlying file descriptor. May be null.
+     */
+    public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset,
+            long length, Bundle extras) {
         if (fd == null) {
             throw new IllegalArgumentException("fd must not be null");
         }
@@ -63,8 +82,9 @@
         mFd = fd;
         mStartOffset = startOffset;
         mLength = length;
+        mExtras = extras;
     }
-    
+
     /**
      * The AssetFileDescriptor contains its own ParcelFileDescriptor, which
      * in addition to the normal FileDescriptor object also allows you to close
@@ -88,7 +108,15 @@
     public long getStartOffset() {
         return mStartOffset;
     }
-    
+
+    /**
+     * Returns any additional details that can be used to interpret the
+     * underlying file descriptor. May be null.
+     */
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
     /**
      * Returns the total number of bytes of this asset entry's data.  May be
      * {@link #UNKNOWN_LENGTH} if the asset extends to the end of the file.
@@ -307,25 +335,37 @@
             super.write(oneByte);
         }
     }
-    
-    
+
     /* Parcelable interface */
+    @Override
     public int describeContents() {
         return mFd.describeContents();
     }
 
+    @Override
     public void writeToParcel(Parcel out, int flags) {
         mFd.writeToParcel(out, flags);
         out.writeLong(mStartOffset);
         out.writeLong(mLength);
+        if (mExtras != null) {
+            out.writeInt(1);
+            out.writeBundle(mExtras);
+        } else {
+            out.writeInt(0);
+        }
     }
 
     AssetFileDescriptor(Parcel src) {
         mFd = ParcelFileDescriptor.CREATOR.createFromParcel(src);
         mStartOffset = src.readLong();
         mLength = src.readLong();
+        if (src.readInt() != 0) {
+            mExtras = src.readBundle();
+        } else {
+            mExtras = null;
+        }
     }
-    
+
     public static final Parcelable.Creator<AssetFileDescriptor> CREATOR
             = new Parcelable.Creator<AssetFileDescriptor>() {
         public AssetFileDescriptor createFromParcel(Parcel in) {
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index c5e4f21..9d35847 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -28,7 +28,9 @@
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
 import android.graphics.Point;
+import android.media.ExifInterface;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.CancellationSignal;
@@ -42,8 +44,10 @@
 import libcore.io.Libcore;
 
 import java.io.BufferedInputStream;
+import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.List;
 
@@ -76,6 +80,15 @@
     /** {@hide} */
     public static final String EXTRA_PACKAGE_NAME = "android.content.extra.PACKAGE_NAME";
 
+    /**
+     * Included in {@link AssetFileDescriptor#getExtras()} when returned
+     * thumbnail should be rotated.
+     *
+     * @see MediaStore.Images.ImageColumns#ORIENTATION
+     * @hide
+     */
+    public static final String EXTRA_ORIENTATION = "android.content.extra.ORIENTATION";
+
     /** {@hide} */
     public static final String ACTION_MANAGE_ROOT = "android.provider.action.MANAGE_ROOT";
     /** {@hide} */
@@ -657,6 +670,7 @@
         openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size);
 
         AssetFileDescriptor afd = null;
+        Bitmap bitmap = null;
         try {
             afd = client.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts, signal);
 
@@ -688,21 +702,36 @@
 
             opts.inJustDecodeBounds = false;
             opts.inSampleSize = Math.min(widthSample, heightSample);
-            Log.d(TAG, "Decoding with sample size " + opts.inSampleSize);
             if (is != null) {
                 is.reset();
-                return BitmapFactory.decodeStream(is, null, opts);
+                bitmap = BitmapFactory.decodeStream(is, null, opts);
             } else {
                 try {
                     Libcore.os.lseek(fd, offset, SEEK_SET);
                 } catch (ErrnoException e) {
                     e.rethrowAsIOException();
                 }
-                return BitmapFactory.decodeFileDescriptor(fd, null, opts);
+                bitmap = BitmapFactory.decodeFileDescriptor(fd, null, opts);
+            }
+
+            // Transform the bitmap if requested. We use a side-channel to
+            // communicate the orientation, since EXIF thumbnails don't contain
+            // the rotation flags of the original image.
+            final Bundle extras = afd.getExtras();
+            final int orientation = (extras != null) ? extras.getInt(EXTRA_ORIENTATION, 0) : 0;
+            if (orientation != 0) {
+                final int width = bitmap.getWidth();
+                final int height = bitmap.getHeight();
+
+                final Matrix m = new Matrix();
+                m.setRotate(orientation, width / 2, height / 2);
+                bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, m, false);
             }
         } finally {
             IoUtils.closeQuietly(afd);
         }
+
+        return bitmap;
     }
 
     /**
@@ -770,4 +799,44 @@
 
         client.call(METHOD_DELETE_DOCUMENT, null, in);
     }
+
+    /**
+     * Open the given image for thumbnail purposes, using any embedded EXIF
+     * thumbnail if available, and providing orientation hints from the parent
+     * image.
+     *
+     * @hide
+     */
+    public static AssetFileDescriptor openImageThumbnail(File file) throws FileNotFoundException {
+        final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
+                file, ParcelFileDescriptor.MODE_READ_ONLY);
+        Bundle extras = null;
+
+        try {
+            final ExifInterface exif = new ExifInterface(file.getAbsolutePath());
+
+            switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) {
+                case ExifInterface.ORIENTATION_ROTATE_90:
+                    extras = new Bundle(1);
+                    extras.putInt(EXTRA_ORIENTATION, 90);
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_180:
+                    extras = new Bundle(1);
+                    extras.putInt(EXTRA_ORIENTATION, 180);
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_270:
+                    extras = new Bundle(1);
+                    extras.putInt(EXTRA_ORIENTATION, 270);
+                    break;
+            }
+
+            final long[] thumb = exif.getThumbnailRange();
+            if (thumb != null) {
+                return new AssetFileDescriptor(pfd, thumb[0], thumb[1], extras);
+            }
+        } catch (IOException e) {
+        }
+
+        return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH, extras);
+    }
 }
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 189e985..11ff2d8 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -27,6 +27,7 @@
 import android.os.ParcelFileDescriptor;
 import android.provider.DocumentsContract.Document;
 import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract;
 import android.provider.DocumentsProvider;
 import android.webkit.MimeTypeMap;
 
@@ -313,19 +314,7 @@
             String documentId, Point sizeHint, CancellationSignal signal)
             throws FileNotFoundException {
         final File file = getFileForDocId(documentId);
-        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);
+        return DocumentsContract.openImageThumbnail(file);
     }
 
     private static String getTypeForFile(File file) {