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