Merge "Show the selected state on an ad on two pane" into jb-ub-mail-ur10
diff --git a/src/com/android/bitmap/DecodeTask.java b/src/com/android/bitmap/DecodeTask.java
index af4674e..2210ec6 100644
--- a/src/com/android/bitmap/DecodeTask.java
+++ b/src/com/android/bitmap/DecodeTask.java
@@ -8,6 +8,9 @@
 import android.os.AsyncTask;
 
 
+import com.android.ex.photo.util.Exif;
+import com.android.mail.utils.RectUtils;
+
 import java.io.IOException;
 import java.io.InputStream;
 
@@ -94,7 +97,10 @@
         AssetFileDescriptor fd = null;
         InputStream in = null;
         try {
-            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+            final boolean isJellyBeanOrAbove = android.os.Build.VERSION.SDK_INT
+                    >= android.os.Build.VERSION_CODES.JELLY_BEAN;
+            // This blocks during fling when the pool is empty. We block early to avoid jank.
+            if (isJellyBeanOrAbove) {
                 Trace.beginSection("poll for reusable bitmap");
                 mInBitmap = mCache.poll();
                 Trace.endSection();
@@ -104,17 +110,65 @@
                 }
             }
 
-            Trace.beginSection("create fd or stream");
+            Trace.beginSection("create fd and stream");
             fd = mKey.createFd();
+            Trace.endSection();
             if (fd == null) {
-                in = mKey.createInputStream();
+                in = reset(in);
+                if (in == null) {
+                    return null;
+                }
+            }
+
+            Trace.beginSection("get bytesize");
+            final long byteSize;
+            if (fd != null) {
+                byteSize = fd.getLength();
+            } else {
+                byteSize = -1;
             }
             Trace.endSection();
 
+            Trace.beginSection("get orientation");
+            if (fd != null) {
+                // Creating an input stream from the file descriptor makes it useless afterwards.
+                Trace.beginSection("create fd and stream");
+                final AssetFileDescriptor orientationFd = mKey.createFd();
+                in = orientationFd.createInputStream();
+                Trace.endSection();
+            }
+            final int orientation = Exif.getOrientation(in, byteSize);
+            if (fd != null) {
+                try {
+                    // Close the temporary file descriptor.
+                    in.close();
+                } catch (IOException ex) {
+                }
+            }
+            final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
+            Trace.endSection();
+
+            if (orientation != 0) {
+                // disable inBitmap-- bitmap reuse doesn't work with different decode regions due
+                // to orientation
+                if (mInBitmap != null) {
+                    mCache.offer(mInBitmap);
+                    mInBitmap = null;
+                    mOpts.inBitmap = null;
+                }
+            }
+
             if (isCancelled()) {
                 return null;
             }
 
+            if (fd == null) {
+                in = reset(in);
+                if (in == null) {
+                    return null;
+                }
+            }
+
             Trace.beginSection("decodeBounds");
             mOpts.inJustDecodeBounds = true;
             if (fd != null) {
@@ -128,20 +182,25 @@
                 return null;
             }
 
-            final int srcW = mOpts.outWidth;
-            final int srcH = mOpts.outHeight;
-
+            // We want to calculate the sample size "as if" the orientation has been corrected.
+            final int srcW, srcH; // Orientation corrected.
+            if (isNotRotatedOr180) {
+                srcW = mOpts.outWidth;
+                srcH = mOpts.outHeight;
+            } else {
+                srcW = mOpts.outHeight;
+                srcH = mOpts.outWidth;
+            }
+            mOpts.inSampleSize = calculateSampleSize(srcW, srcH, mDestW, mDestH);
             mOpts.inJustDecodeBounds = false;
             mOpts.inMutable = true;
-            mOpts.inSampleSize = calculateSampleSize(srcW, srcH, mDestW, mDestH);
-            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+            if (isJellyBeanOrAbove && orientation == 0) {
                 if (mInBitmap == null) {
                     if (DEBUG) System.err.println(
                             "decode thread wants a bitmap. cache dump:\n" + mCache.toDebugString());
                     Trace.beginSection("create reusable bitmap");
-                    mInBitmap = new ReusableBitmap(
-                            Bitmap.createBitmap(mDestBufferW, mDestBufferH,
-                                    Bitmap.Config.ARGB_8888));
+                    mInBitmap = new ReusableBitmap(Bitmap.createBitmap(mDestBufferW, mDestBufferH,
+                            Bitmap.Config.ARGB_8888));
                     Trace.endSection();
 
                     if (isCancelled()) {
@@ -158,21 +217,23 @@
                 mOpts.inBitmap = mInBitmap.bmp;
             }
 
-            Bitmap decodeResult = null;
-
-            if (in != null) {
-                in = mKey.createInputStream();
-            }
-
             if (isCancelled()) {
                 return null;
             }
 
-            final Rect srcRect = new Rect();
+            if (fd == null) {
+                in = reset(in);
+                if (in == null) {
+                    return null;
+                }
+            }
+
+            Bitmap decodeResult = null;
+            final Rect srcRect = new Rect(); // Not orientation corrected. True coordinates.
             if (CROP_DURING_DECODE) {
                 try {
                     Trace.beginSection("decodeCropped" + mOpts.inSampleSize);
-                    decodeResult = decodeCropped(fd, in, srcRect);
+                    decodeResult = decodeCropped(fd, in, orientation, srcRect);
                 } catch (IOException e) {
                     // fall through to below and try again with the non-cropping decoder
                     e.printStackTrace();
@@ -213,36 +274,33 @@
                 }
             }
 
-            if (decodeResult != null) {
-                if (mInBitmap != null) {
-                    result = mInBitmap;
-                    // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath
-                    if (!srcRect.isEmpty()) {
-                        result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize);
-                        result.setLogicalHeight(
-                                (srcRect.bottom - srcRect.top) / mOpts.inSampleSize);
-                    } else {
-                        result.setLogicalWidth(mOpts.outWidth);
-                        result.setLogicalHeight(mOpts.outHeight);
-                    }
+            if (decodeResult == null) {
+                return null;
+            }
+
+            if (mInBitmap != null) {
+                result = mInBitmap;
+                // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath
+                if (!srcRect.isEmpty()) {
+                    result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize);
+                    result.setLogicalHeight(
+                            (srcRect.bottom - srcRect.top) / mOpts.inSampleSize);
                 } else {
-                    // no mInBitmap means no pooling
-                    result = new ReusableBitmap(decodeResult, false /* reusable */);
+                    result.setLogicalWidth(mOpts.outWidth);
+                    result.setLogicalHeight(mOpts.outHeight);
+                }
+            } else {
+                // no mInBitmap means no pooling
+                result = new ReusableBitmap(decodeResult, false /* reusable */);
+                if (isNotRotatedOr180) {
                     result.setLogicalWidth(decodeResult.getWidth());
                     result.setLogicalHeight(decodeResult.getHeight());
+                } else {
+                    result.setLogicalWidth(decodeResult.getHeight());
+                    result.setLogicalHeight(decodeResult.getWidth());
                 }
-                // System.out.println("*** async task decoded fd=" + mUri +
-                // " to" +
-                // " sz=" + (decodeResult.getByteCount() >> 10) + "KB dstW/H=" +
-                // result.getLogicalWidth() +
-                // "/" + result.getLogicalHeight() + " srcW/H=" + srcW + "/" +
-                // srcH + " ss=" +
-                // mOpts.inSampleSize + " mutable=" + decodeResult.isMutable() +
-                // " matchesInBitmap=" + (mOpts.inBitmap == decodeResult));
-            } else {
-                // System.out.println("*** async task cancelled decode of fd=" +
-                // mUri);
             }
+            result.setOrientation(orientation);
         } catch (Exception e) {
             e.printStackTrace();
         } finally {
@@ -250,14 +308,12 @@
                 try {
                     fd.close();
                 } catch (IOException e) {
-                    e.printStackTrace();
                 }
             }
             if (in != null) {
                 try {
                     in.close();
                 } catch (IOException e) {
-                    e.printStackTrace();
                 }
             }
             if (result != null) {
@@ -274,40 +330,72 @@
         return result;
     }
 
-    private Bitmap decodeCropped(AssetFileDescriptor fd, InputStream in, Rect outSrcRect)
-            throws IOException {
+    private Bitmap decodeCropped(final AssetFileDescriptor fd, final InputStream in,
+            final int orientation, final Rect outSrcRect) throws IOException {
         final BitmapRegionDecoder brd;
         if (fd != null) {
-            brd = BitmapRegionDecoder.newInstance(fd.getFileDescriptor(),
-                    true /* shareable */);
+            brd = BitmapRegionDecoder.newInstance(fd.getFileDescriptor(), true /* shareable */);
         } else {
-            brd = BitmapRegionDecoder.newInstance(in,
-                    true /* shareable */);
+            brd = BitmapRegionDecoder.newInstance(in, true /* shareable */);
         }
         if (isCancelled()) {
             brd.recycle();
             return null;
         }
 
-        final int srcW = mOpts.outWidth;
-        final int srcH = mOpts.outHeight;
+        // We want to call calculateCroppedSrcRect() on the source rectangle "as if" the
+        // orientation has been corrected.
+        final int srcW, srcH; //Orientation corrected.
+        final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
+        if (isNotRotatedOr180) {
+            srcW = mOpts.outWidth;
+            srcH = mOpts.outHeight;
+        } else {
+            srcW = mOpts.outHeight;
+            srcH = mOpts.outWidth;
+        }
 
-        // Trace.beginSection("DecodeRegionGetDimens");
-        // final int tmpw = brd.getWidth();
-        // final int tmph = brd.getHeight();
-        // Trace.endSection();
-
-        // Center the decode on the top 1/3
+        // Coordinates are orientation corrected.
+        // Center the decode on the top 1/3.
         BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDestW, mDestH, mDestH, mOpts.inSampleSize,
                 1f / 3, true /* absoluteFraction */, 1f, outSrcRect);
         if (DEBUG) System.out.println("rect for this decode is: " + outSrcRect
                 + " srcW/H=" + srcW + "/" + srcH
                 + " dstW/H=" + mDestW + "/" + mDestH);
+
+        // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
+        // been corrected. We need to decode the uncorrected source rectangle. Calculate true
+        // coordinates.
+        RectUtils.rotateRectForOrientation(orientation, new Rect(0, 0, srcW, srcH), outSrcRect);
+
         final Bitmap result = brd.decodeRegion(outSrcRect, mOpts);
         brd.recycle();
         return result;
     }
 
+    /**
+     * Return an input stream that can be read from the beginning using the most efficient way,
+     * given an input stream that may or may not support reset(), or given null.
+     *
+     * The returned input stream may or may not be the same stream.
+     */
+    private InputStream reset(InputStream in) throws IOException {
+        Trace.beginSection("create stream");
+        if (in == null) {
+            in = mKey.createInputStream();
+        } else if (in.markSupported()) {
+            in.reset();
+        } else {
+            try {
+                in.close();
+            } catch (IOException ex) {
+            }
+            in = mKey.createInputStream();
+        }
+        Trace.endSection();
+        return in;
+    }
+
     private Bitmap decode(AssetFileDescriptor fd, InputStream in) {
         final Bitmap result;
         if (fd != null) {
diff --git a/src/com/android/bitmap/ReusableBitmap.java b/src/com/android/bitmap/ReusableBitmap.java
index 846eda0..2e0199f 100644
--- a/src/com/android/bitmap/ReusableBitmap.java
+++ b/src/com/android/bitmap/ReusableBitmap.java
@@ -27,14 +27,16 @@
     public final Bitmap bmp;
     private int mWidth;
     private int mHeight;
+    private int mOrientation;
+
     private int mRefCount = 0;
     private final boolean mReusable;
 
-    public ReusableBitmap(Bitmap bitmap) {
+    public ReusableBitmap(final Bitmap bitmap) {
         this(bitmap, true /* reusable */);
     }
 
-    public ReusableBitmap(Bitmap bitmap, boolean reusable) {
+    public ReusableBitmap(final Bitmap bitmap, final boolean reusable) {
         bmp = bitmap;
         mReusable = reusable;
     }
@@ -60,6 +62,14 @@
         return mHeight;
     }
 
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+    public void setOrientation(final int orientation) {
+        mOrientation = orientation;
+    }
+
     public int getByteCount() {
         return bmp.getByteCount();
     }
diff --git a/src/com/android/mail/bitmap/AttachmentDrawable.java b/src/com/android/mail/bitmap/AttachmentDrawable.java
index 927561a..f350caa 100644
--- a/src/com/android/mail/bitmap/AttachmentDrawable.java
+++ b/src/com/android/mail/bitmap/AttachmentDrawable.java
@@ -27,6 +27,7 @@
 import com.android.mail.browse.ConversationItemViewCoordinates;
 import com.android.mail.ui.SwipeableListView;
 import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.RectUtils;
 
 import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -194,7 +195,7 @@
     }
 
     @Override
-    public void draw(Canvas canvas) {
+    public void draw(final Canvas canvas) {
         final Rect bounds = getBounds();
         if (bounds.isEmpty()) {
             return;
@@ -207,7 +208,24 @@
                             mCoordinates.attachmentPreviewsDecodeHeight, Integer.MAX_VALUE,
                             mParallaxFraction, false /* absoluteFraction */,
                             mParallaxSpeedMultiplier, mSrcRect);
-            canvas.drawBitmap(mBitmap.bmp, mSrcRect, bounds, mPaint);
+
+            final int orientation = mBitmap.getOrientation();
+            // calculateCroppedSrcRect() gave us the source rectangle "as if" the orientation has
+            // been corrected. We need to decode the uncorrected source rectangle. Calculate true
+            // coordinates.
+            RectUtils.rotateRectForOrientation(orientation,
+                    new Rect(0, 0, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight()),
+                    mSrcRect);
+
+            // We may need to rotate the canvas, so we also have to rotate the bounds.
+            final Rect rotatedBounds = new Rect(bounds);
+            RectUtils.rotateRect(orientation, bounds.centerX(), bounds.centerY(), rotatedBounds);
+
+            // Rotate the canvas.
+            canvas.save();
+            canvas.rotate(orientation, bounds.centerX(), bounds.centerY());
+            canvas.drawBitmap(mBitmap.bmp, mSrcRect, rotatedBounds, mPaint);
+            canvas.restore();
         }
 
         // Draw the two possible overlay layers in reverse-priority order.
diff --git a/src/com/android/mail/bitmap/ImageAttachmentRequest.java b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
index d60552b..6c58772 100644
--- a/src/com/android/mail/bitmap/ImageAttachmentRequest.java
+++ b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
@@ -97,7 +97,7 @@
     }
 
     @Override
-    public InputStream createInputStream() {
+    public InputStream createInputStream() throws IOException {
         return null;
     }
 }
diff --git a/src/com/android/mail/ui/ThumbnailLoadTask.java b/src/com/android/mail/ui/ThumbnailLoadTask.java
index e4b8c20..c30864e 100644
--- a/src/com/android/mail/ui/ThumbnailLoadTask.java
+++ b/src/com/android/mail/ui/ThumbnailLoadTask.java
@@ -33,7 +33,6 @@
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 
@@ -164,27 +163,11 @@
             return 0;
         }
 
-        ByteArrayOutputStream out = null;
         InputStream in = null;
         try {
             final ContentResolver resolver = mHolder.getResolver();
             in = resolver.openInputStream(thumbnailUri);
-            out = new ByteArrayOutputStream();
-            final byte[] buffer = new byte[4096];
-            int n = in.read(buffer);
-            while (n >= 0) {
-                out.write(buffer, 0, n);
-                n = in.read(buffer);
-            }
-            in.close();
-            in = null;
-
-            if (isCancelled()) {
-                return 0;
-            }
-
-            final byte[] bitmapBytes = out.toByteArray();
-            return Exif.getOrientation(bitmapBytes);
+            return Exif.getOrientation(in, -1);
         } catch (Throwable t) {
             LogUtils.e(LOG_TAG, t, "Unable to get orientation of thumbnail %s", thumbnailUri);
         } finally {
@@ -195,13 +178,6 @@
                     LogUtils.e(LOG_TAG, e, "error attemtping to close input stream");
                 }
             }
-            if (out != null) {
-                try {
-                    out.close();
-                } catch (IOException e) {
-                    LogUtils.e(LOG_TAG, e, "error attemtping to close output stream");
-                }
-            }
         }
 
         return 0;
diff --git a/src/com/android/mail/utils/RectUtils.java b/src/com/android/mail/utils/RectUtils.java
new file mode 100644
index 0000000..43d070d
--- /dev/null
+++ b/src/com/android/mail/utils/RectUtils.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2013, Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.utils;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+public class RectUtils {
+
+    /**
+     * Transform the upright full rectangle so that it bounds the original rotated image,
+     * given by the orientation. Transform the upright partial rectangle such that it would apply
+     * to the same region of the transformed full rectangle.
+     *
+     * The top-left of the transformed full rectangle will always be placed at (0, 0).
+     * @param orientation The exif orientation (0, 90, 180, 270) of the original image. The
+     *                    transformed full and partial rectangles will be in this orientation's
+     *                    coordinate space.
+     * @param fullRect    The upright full rectangle. This rectangle will be modified.
+     * @param partialRect The upright partial rectangle. This rectangle will be modified.
+     */
+    public static void rotateRectForOrientation(final int orientation, final Rect fullRect,
+            final Rect partialRect) {
+        final Matrix matrix = new Matrix();
+        // Exif orientation specifies how the camera is rotated relative to the actual subject.
+        // First rotate in the opposite direction.
+        matrix.setRotate(-orientation);
+        final RectF fullRectF = new RectF(fullRect);
+        final RectF partialRectF = new RectF(partialRect);
+        matrix.mapRect(fullRectF);
+        matrix.mapRect(partialRectF);
+        // Then translate so that the upper left corner of the rotated full rect is at (0,0).
+        matrix.reset();
+        matrix.setTranslate(-fullRectF.left, -fullRectF.top);
+        matrix.mapRect(fullRectF);
+        matrix.mapRect(partialRectF);
+        // Orientation transformation is complete.
+        fullRect.set((int) fullRectF.left, (int) fullRectF.top, (int) fullRectF.right,
+                (int) fullRectF.bottom);
+        partialRect.set((int) partialRectF.left, (int) partialRectF.top, (int) partialRectF.right,
+                (int) partialRectF.bottom);
+    }
+
+    public static void rotateRect(final int degrees, final int px, final int py, final Rect rect) {
+        final RectF rectF = new RectF(rect);
+        final Matrix matrix = new Matrix();
+        matrix.setRotate(degrees, px, py);
+        matrix.mapRect(rectF);
+        rect.set((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom);
+    }
+}