move image decode into background thread

Fixes strict mode violations. Now it also handles arbitrarily
large full-size images, which is all EAS/IMAP may offer.

Use contentUri when thumbnailUri is not populated (as is the
case with EAS/IMAP).

Bug: 6195064
Bug: 6389109
Change-Id: Ifcc2062d35e77c0fca4e6a568488ab556ba0da08
diff --git a/res/layout/conversation_message_attachment.xml b/res/layout/conversation_message_attachment.xml
index eb15614..df0a466 100644
--- a/res/layout/conversation_message_attachment.xml
+++ b/res/layout/conversation_message_attachment.xml
@@ -32,7 +32,7 @@
             android:id="@+id/attachment_icon"
             android:layout_width="48dp"
             android:layout_height="match_parent"
-            android:scaleType="fitCenter"
+            android:scaleType="centerCrop"
             android:background="#e5e5e5" />
 
         <RelativeLayout
diff --git a/src/com/android/mail/browse/MessageFooterView.java b/src/com/android/mail/browse/MessageFooterView.java
index 39a459e..a4c5e13 100644
--- a/src/com/android/mail/browse/MessageFooterView.java
+++ b/src/com/android/mail/browse/MessageFooterView.java
@@ -116,7 +116,7 @@
     }
 
     private void renderAttachments() {
-        List<Attachment> attachments;
+        final List<Attachment> attachments;
         if (mAttachmentsCursor != null && !mAttachmentsCursor.isClosed()) {
             int i = -1;
             attachments = Lists.newArrayList();
diff --git a/src/com/android/mail/browse/MessageHeaderAttachment.java b/src/com/android/mail/browse/MessageHeaderAttachment.java
index 634f64d..453508b 100644
--- a/src/com/android/mail/browse/MessageHeaderAttachment.java
+++ b/src/com/android/mail/browse/MessageHeaderAttachment.java
@@ -25,6 +25,11 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -49,6 +54,8 @@
 import com.android.mail.utils.MimeType;
 import com.android.mail.utils.Utils;
 
+import java.io.IOException;
+
 /**
  * View for a single attachment in conversation view. Shows download status and allows launching
  * intents to act on an attachment.
@@ -60,6 +67,7 @@
 
     private Attachment mAttachment;
     private ImageView mIcon;
+    private ImageView.ScaleType mIconScaleType;
     private TextView mTitle;
     private TextView mSubTitle;
     private String mAttachmentSizeText;
@@ -75,6 +83,8 @@
     private Button mInstallButton;
     private Button mCancelButton;
 
+    private ThumbnailLoadTask mThumbnailTask;
+
     private static final String LOG_TAG = new LogUtils().getLogTag();
 
     private class AttachmentCommandHandler extends AsyncQueryHandler {
@@ -93,6 +103,81 @@
 
     }
 
+    private class ThumbnailLoadTask extends AsyncTask<Uri, Void, Bitmap> {
+
+        private final int mWidth;
+        private final int mHeight;
+
+        public ThumbnailLoadTask(int width, int height) {
+            mWidth = width;
+            mHeight = height;
+        }
+
+        @Override
+        protected Bitmap doInBackground(Uri... params) {
+            final Uri thumbnailUri = params[0];
+
+            AssetFileDescriptor fd = null;
+            Bitmap result = null;
+
+            try {
+                fd = getContext().getContentResolver().openAssetFileDescriptor(thumbnailUri, "r");
+                if (isCancelled() || fd == null) {
+                    return null;
+                }
+
+                final BitmapFactory.Options opts = new BitmapFactory.Options();
+                opts.inJustDecodeBounds = true;
+
+                BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, opts);
+                if (isCancelled() || opts.outWidth == -1 || opts.outHeight == -1) {
+                    return null;
+                }
+
+                opts.inJustDecodeBounds = false;
+                // Shrink both X and Y (but do not over-shrink)
+                // and pick the least affected dimension to ensure the thumbnail is fillable
+                // (i.e. ScaleType.CENTER_CROP)
+                final int wDivider = Math.max(opts.outWidth / mWidth, 1);
+                final int hDivider = Math.max(opts.outHeight / mHeight, 1);
+                opts.inSampleSize = Math.min(wDivider, hDivider);
+
+                LogUtils.d(LOG_TAG, "in background, src w/h=%d/%d dst w/h=%d/%d, divider=%d",
+                        opts.outWidth, opts.outHeight, mWidth, mHeight, opts.inSampleSize);
+
+                result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, opts);
+
+            } catch (Throwable t) {
+                LogUtils.e(LOG_TAG, t, "Unable to decode thumbnail %s", thumbnailUri);
+            } finally {
+                if (fd != null) {
+                    try {
+                        fd.close();
+                    } catch (IOException e) {
+                        LogUtils.e(LOG_TAG, e, "");
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap result) {
+            if (result == null) {
+                LogUtils.d(LOG_TAG, "back in UI thread, decode failed");
+                setThumbnailToDefault();
+                return;
+            }
+
+            LogUtils.d(LOG_TAG, "back in UI thread, decode success, w/h=%d/%d", result.getWidth(),
+                    result.getHeight());
+            mIcon.setImageBitmap(result);
+            mIcon.setScaleType(mIconScaleType);
+        }
+
+    }
+
     public MessageHeaderAttachment(Context context) {
         super(context);
     }
@@ -135,18 +220,21 @@
             updateSubtitleText(null);
         }
 
-        if (attachment.isImage() && attachment.thumbnailUri != null) {
-            if (prevAttachment == null || prevAttachment.thumbnailUri == null ||
-                    !attachment.thumbnailUri.equals(prevAttachment.thumbnailUri)) {
-                // FIXME: this decodes on the UI thread. Also, it doesn't handle large images, so
-                // using the full image is out of the question.
-                mIcon.setImageURI(attachment.thumbnailUri);
+        final Uri imageUri = attachment.getImageUri();
+        final Uri prevImageUri = (prevAttachment == null) ? null : prevAttachment.getImageUri();
+        // begin loading a thumbnail if this is an image and either the thumbnail or the original
+        // content is ready (and different from any existing image)
+        if (imageUri != null && (prevImageUri == null || !imageUri.equals(prevImageUri))) {
+            // cancel/dispose any existing task and start a new one
+            if (mThumbnailTask != null) {
+                mThumbnailTask.cancel(true);
             }
-        }
-        if (mIcon.getDrawable() == null) {
-            // not an image, or image load failed. fall back to default.
-            mIcon.setImageResource(R.drawable.ic_menu_attachment_holo_light);
-            mIcon.setScaleType(ImageView.ScaleType.CENTER);
+            mThumbnailTask = new ThumbnailLoadTask(mIcon.getWidth(), mIcon.getHeight());
+            mThumbnailTask.execute(imageUri);
+        } else {
+            // not an image, or no thumbnail exists. fall back to default.
+            // async image load must separately ensure the default appears upon load failure.
+            setThumbnailToDefault();
         }
 
         mProgress.setMax(attachment.size);
@@ -155,6 +243,11 @@
         updateStatus();
     }
 
+    private void setThumbnailToDefault() {
+        mIcon.setImageResource(R.drawable.ic_menu_attachment_holo_light);
+        mIcon.setScaleType(ImageView.ScaleType.CENTER);
+    }
+
     /**
      * Update progress-related views. Will also trigger a view intent if a progress dialog was
      * previously brought up (by tapping 'View') and the download has now finished.
@@ -242,6 +335,8 @@
         mPlayButton.setOnClickListener(this);
         mInstallButton.setOnClickListener(this);
         mCancelButton.setOnClickListener(this);
+
+        mIconScaleType = mIcon.getScaleType();
     }
 
     @Override
diff --git a/src/com/android/mail/providers/Attachment.java b/src/com/android/mail/providers/Attachment.java
index 4558ca2..11a8679 100644
--- a/src/com/android/mail/providers/Attachment.java
+++ b/src/com/android/mail/providers/Attachment.java
@@ -261,6 +261,21 @@
                 !isSavedToExternal();
     }
 
+    /**
+     * If this attachment is an image, returns a Uri pointing to the image that can be used as a
+     * thumbnail. If the provider supports dedicated thumbnails, it will be relatively small, but
+     * if not, the image may be arbitrarily large. Client code must handle this efficiently. For
+     * non-image attachments, this method will return null. This method may also return null if the
+     * attachment is not yet downloaded.
+     */
+    public Uri getImageUri() {
+        if (!isImage()) {
+            return null;
+        }
+
+        return (thumbnailUri != null) ? thumbnailUri : contentUri;
+    }
+
     // Methods to support JSON [de-]serialization of Attachment data
     // TODO: add support for origin/originExtras (and possibly partId?) or fold those fields into
     // other fields so Compose View can use JSON objects