Collapsing attachments in compose now works.

When the draft has attachments, if the focus
on any of the text fields changes, the attachment
area collapses into a smaller area. Tapping that area
causes the attachments to expand into the full view
with the previews. Some refactoring was necessary to
enable reuse of the thumbnail loading for the small
preview icon.

Change-Id: Iaefb71736a2359b9090b2fe736ad2f33a9838ed7
diff --git a/res/layout-sw600dp/compose.xml b/res/layout-sw600dp/compose.xml
index 92c6db3..dd00a30 100644
--- a/res/layout-sw600dp/compose.xml
+++ b/res/layout-sw600dp/compose.xml
@@ -69,16 +69,7 @@
                 android:paddingBottom="0dip"
                 android:paddingLeft="0dip"
                 android:visibility="gone" >
-                <com.android.mail.ui.AttachmentTileGrid
-                    android:id="@+id/attachment_tile_grid"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:layout_marginBottom="8dip" />
-                <LinearLayout
-                    android:id="@+id/attachment_bar_list"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:orientation="vertical" />
+                <include layout="@layout/compose_attachments" />
             </com.android.mail.compose.AttachmentsView>
 
             <!-- Compose Area -->
diff --git a/res/layout/compose.xml b/res/layout/compose.xml
index ede99df..5d786ca 100644
--- a/res/layout/compose.xml
+++ b/res/layout/compose.xml
@@ -56,17 +56,9 @@
                 android:paddingTop="8dip"
                 android:paddingRight="5dip"
                 android:paddingBottom="0dip"
-                android:paddingLeft="5dip">
-                <com.android.mail.ui.AttachmentTileGrid
-                    android:id="@+id/attachment_tile_grid"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:layout_marginBottom="8dip" />
-                <LinearLayout
-                    android:id="@+id/attachment_bar_list"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:orientation="vertical" />
+                android:paddingLeft="5dip"
+                android:visibility="gone">
+                <include layout="@layout/compose_attachments" />
             </com.android.mail.compose.AttachmentsView>
             <!-- Body -->
             <include layout="@layout/compose_body"/>
diff --git a/res/layout/compose_attachments.xml b/res/layout/compose_attachments.xml
new file mode 100644
index 0000000..382db81
--- /dev/null
+++ b/res/layout/compose_attachments.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2012 Google Inc.
+     Licensed to The Android Open Source Project.
+
+     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.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <GridLayout
+        android:id="@+id/attachment_collapse_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:background="@drawable/attachment_bg_holo"
+        android:rowCount="1"
+        android:columnCount="3"
+        android:visibility="gone">
+        <ImageView
+            android:id="@+id/attachment_collapse_preview_icon"
+            android:layout_width="48dip"
+            android:layout_height="48dip"
+            android:layout_row="0"
+            android:layout_column="0"
+            android:scaleType="center"
+            android:src="@drawable/ic_menu_attachment_holo_light"/>
+        <TextView
+            android:id="@+id/attachment_collapse_text"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_row="0"
+            android:layout_column="1"
+            android:layout_gravity="fill_horizontal|center_vertical"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textAllCaps="true"
+            android:layout_marginLeft="8dip"
+            android:singleLine="true"
+            android:ellipsize="end" />
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_row="0"
+            android:layout_column="2"
+            android:layout_gravity="center_vertical"
+            android:layout_marginRight="8dip"
+            android:src="@drawable/ic_menu_expander_minimized_holo_light"/>
+        <View
+            android:layout_width="0dip"
+            android:layout_height="0dip"
+            android:layout_row="0"
+            android:layout_column="0"
+            android:layout_columnSpan="3"
+            android:layout_gravity="fill"
+            android:background="?android:attr/selectableItemBackground"
+            />
+    </GridLayout>
+    <com.android.mail.ui.AttachmentTileGrid
+        android:id="@+id/attachment_tile_grid"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="8dip" />
+    <LinearLayout
+        android:id="@+id/attachment_bar_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical" />
+</merge>
\ No newline at end of file
diff --git a/res/layout/conversation_message_attachment_bar.xml b/res/layout/conversation_message_attachment_bar.xml
index d578509..f4076c1 100644
--- a/res/layout/conversation_message_attachment_bar.xml
+++ b/res/layout/conversation_message_attachment_bar.xml
@@ -33,7 +33,7 @@
         android:layout_rowSpan="3"
         android:scaleType="center"
         android:src="@drawable/ic_menu_attachment_holo_light"
-        android:background="#e5e5e5" />
+        android:background="@color/attachment_image_background_color" />
 
     <TextView
         android:id="@+id/attachment_title"
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 036c57f..a2fb47c 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -74,4 +74,5 @@
     <color name="attachment_tile_shadow_box_color">#7F000000</color>
     <!--  Color of the subtitle message in the attachment tile -->
     <color name="attachment_tile_subtitle_color">#CCCCCC</color>
+    <color name="attachment_image_background_color">#E5E5E5</color>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 52fc007..2a4e64e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -248,13 +248,13 @@
     <!-- Displayed in the action bar as a subtitle. Save in progress. [CHAR LIMIT=15] -->
     <string name="saving">Saving&#8230;</string>
     <!-- Attachment Type Selector Adapter. This string is an option in the "Attach" dialog [CHAR LIMIT=20] -->
-    <string name="attach_image">Pictures</string>
+    <string name="attach_image">Picture</string>
     <!-- Attachment Type Selector Adapter. This string is an option in the "Attach" dialog [CHAR LIMIT=20] -->
-    <string name="attach_take_photo">Capture picture</string>
-    <!-- Attachment Type Selector Adapter. This string is an option in the "Attach" dialog [CHAR LIMIT=20] -->
-    <string name="attach_video">Videos</string>
-    <!-- Attachment Type Selector Adapter. This string is an option in the "Attach" dialog [CHAR LIMIT=20] -->
-    <string name="attach_record_video">Capture video</string>
+    <string name="attach_video">Video</string>
+    <plurals name="number_of_attachments">
+        <item quantity="one">%d attachment</item>
+        <item quantity="other">%d attachments</item>
+    </plurals>
 
     <!-- Webview Context Menu Strings -->
     <!-- Title of dialog for choosing which activity to share a link with. [CHAR LIMIT=50]-->
diff --git a/src/com/android/mail/compose/AttachmentsView.java b/src/com/android/mail/compose/AttachmentsView.java
index d1615c2..2ea0581 100644
--- a/src/com/android/mail/compose/AttachmentsView.java
+++ b/src/com/android/mail/compose/AttachmentsView.java
@@ -17,8 +17,10 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.ParcelFileDescriptor;
 import android.provider.OpenableColumns;
@@ -26,8 +28,12 @@
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
+import android.widget.GridLayout;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import com.android.mail.R;
@@ -35,8 +41,10 @@
 import com.android.mail.providers.Attachment;
 import com.android.mail.providers.Message;
 import com.android.mail.providers.UIProvider;
+import com.android.mail.ui.AttachmentBitmapHolder;
 import com.android.mail.ui.AttachmentTile;
 import com.android.mail.ui.AttachmentTileGrid;
+import com.android.mail.ui.ThumbnailLoadTask;
 import com.android.mail.utils.LogUtils;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
@@ -48,12 +56,21 @@
 /*
  * View for displaying attachments in the compose screen.
  */
-class AttachmentsView extends LinearLayout {
+class AttachmentsView extends LinearLayout implements AttachmentBitmapHolder, OnClickListener {
     private static final String LOG_TAG = new LogUtils().getLogTag();
+
+    private final Resources mResources;
+
     private ArrayList<Attachment> mAttachments;
     private AttachmentDeletedListener mChangeListener;
     private AttachmentTileGrid mTileGrid;
     private LinearLayout mAttachmentLayout;
+    private GridLayout mCollapseLayout;
+    private TextView mCollapseText;
+    private ImageView mCollapseImage;
+
+    private ThumbnailLoadTask mThumbnailTask;
+    private Attachment mPreviewAttachment;
 
     public AttachmentsView(Context context) {
         this(context, null);
@@ -62,6 +79,7 @@
     public AttachmentsView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mAttachments = Lists.newArrayList();
+        mResources = context.getResources();
     }
 
     @Override
@@ -70,6 +88,58 @@
 
         mTileGrid = (AttachmentTileGrid) findViewById(R.id.attachment_tile_grid);
         mAttachmentLayout = (LinearLayout) findViewById(R.id.attachment_bar_list);
+        mCollapseLayout = (GridLayout) findViewById(R.id.attachment_collapse_view);
+        mCollapseText = (TextView) findViewById(R.id.attachment_collapse_text);
+        mCollapseImage = (ImageView) findViewById(R.id.attachment_collapse_preview_icon);
+
+        mCollapseLayout.setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View view) {
+        switch (view.getId()) {
+            case R.id.attachment_collapse_view:
+                expandView();
+                break;
+        }
+    }
+
+    public void collapseView() {
+        mTileGrid.setVisibility(GONE);
+        mAttachmentLayout.setVisibility(GONE);
+
+        // If there are some attachments, show the preview
+        if (!mAttachments.isEmpty()) {
+            // setup text
+            final int numAttachments = mAttachments.size();
+            final String attachmentText = mResources.getQuantityString(
+                    R.plurals.number_of_attachments, numAttachments, numAttachments);
+            mCollapseText.setText(attachmentText);
+
+            // setup icon
+            final Attachment previousAttachment = mPreviewAttachment;
+            mPreviewAttachment = getFirstTiledAttachment();
+            ThumbnailLoadTask.setupThumbnailPreview(
+                    mThumbnailTask, this, mPreviewAttachment, previousAttachment);
+
+            mCollapseLayout.setVisibility(VISIBLE);
+        }
+    }
+
+    private Attachment getFirstTiledAttachment() {
+        for (final Attachment attachment : mAttachments) {
+            if (AttachmentTile.isTiledAttachment(attachment)) {
+                return attachment;
+            }
+        }
+
+        return null;
+    }
+
+    public void expandView() {
+        mTileGrid.setVisibility(VISIBLE);
+        mAttachmentLayout.setVisibility(VISIBLE);
+        mCollapseLayout.setVisibility(GONE);
     }
 
     /**
@@ -87,6 +157,7 @@
     public void addAttachment(final Attachment attachment) {
         if (!isShown()) {
             setVisibility(View.VISIBLE);
+            expandView();
         }
         mAttachments.add(attachment);
 
@@ -131,6 +202,7 @@
         }
         if (mAttachments.size() == 0) {
             setVisibility(View.GONE);
+            collapseView();
         }
     }
 
@@ -386,4 +458,29 @@
             super(detailMessage, throwable);
         }
     }
+
+    @Override
+    public int getThumbnailWidth() {
+        return mCollapseImage.getWidth();
+    }
+
+    @Override
+    public int getThumbnailHeight() {
+        return mCollapseImage.getHeight();
+    }
+
+    @Override
+    public void setThumbnail(Bitmap result) {
+        mCollapseImage.setImageBitmap(result);
+    }
+
+    @Override
+    public void setThumbnailToDefault() {
+        mCollapseImage.setImageResource(R.drawable.ic_menu_attachment_holo_light);
+    }
+
+    @Override
+    public ContentResolver getResolver() {
+        return getContext().getContentResolver();
+    }
 }
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index d174a61..6a34ee8 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -48,6 +48,7 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
 import android.view.ViewGroup;
 import android.view.inputmethod.BaseInputConnection;
 import android.widget.ArrayAdapter;
@@ -97,7 +98,7 @@
 
 public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
         RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
-        AttachmentDeletedListener, OnAccountChangedListener {
+        AttachmentDeletedListener, OnAccountChangedListener, OnFocusChangeListener {
     // Identifiers for which type of composition this is
     static final int COMPOSE = -1;
     static final int REPLY = 0;
@@ -275,6 +276,7 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.compose);
         findViews();
+        setFocusListeners();
         Intent intent = getIntent();
         Account account = null;
         Message message;
@@ -782,6 +784,22 @@
         mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
     }
 
+    private void setFocusListeners() {
+        mTo.setOnFocusChangeListener(this);
+        mCc.setOnFocusChangeListener(this);
+        mBcc.setOnFocusChangeListener(this);
+        mSubject.setOnFocusChangeListener(this);
+        mQuotedTextView.setOnFocusChangeListener(this);
+        mBodyView.setOnFocusChangeListener(this);
+    }
+
+    @Override
+    public void onFocusChange(View v, boolean hasFocus) {
+        if (hasFocus) {
+            mAttachmentsView.collapseView();
+        }
+    }
+
     protected TextView getBody() {
         return mBodyView;
     }
diff --git a/src/com/android/mail/ui/AttachmentBitmapHolder.java b/src/com/android/mail/ui/AttachmentBitmapHolder.java
new file mode 100644
index 0000000..4c32945
--- /dev/null
+++ b/src/com/android/mail/ui/AttachmentBitmapHolder.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * 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.ui;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+
+/**
+ * Interface to enable reuse of {@link ThumbnailLoadTask} with various
+ * different views.
+ */
+public interface AttachmentBitmapHolder {
+    public int getThumbnailWidth();
+    public int getThumbnailHeight();
+    public void setThumbnail(Bitmap result);
+    public void setThumbnailToDefault();
+    public ContentResolver getResolver();
+}
diff --git a/src/com/android/mail/ui/AttachmentTile.java b/src/com/android/mail/ui/AttachmentTile.java
index 6e9de71..87d19e3 100644
--- a/src/com/android/mail/ui/AttachmentTile.java
+++ b/src/com/android/mail/ui/AttachmentTile.java
@@ -17,12 +17,10 @@
 
 package com.android.mail.ui;
 
+import android.content.ContentResolver;
 import android.content.Context;
-import android.content.res.AssetFileDescriptor;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.ImageView;
@@ -33,13 +31,11 @@
 import com.android.mail.providers.Attachment;
 import com.android.mail.utils.LogUtils;
 
-import java.io.IOException;
-
 /**
  * Base class for attachment tiles that handles the work
  * of fetching and displaying the bitmaps for the tiles.
  */
-public class AttachmentTile extends RelativeLayout {
+public class AttachmentTile extends RelativeLayout implements AttachmentBitmapHolder {
     protected Attachment mAttachment;
     private ImageView mIcon;
     private ImageView.ScaleType mIconScaleType;
@@ -93,99 +89,29 @@
                 attachment.destination, attachment.downloadedSize, attachment.contentUri,
                 attachment.contentType);
 
-        setupThumbnailPreview(attachment, prevAttachment);
+        ThumbnailLoadTask.setupThumbnailPreview(mThumbnailTask, this, attachment, prevAttachment);
     }
 
-    private void setupThumbnailPreview(Attachment attachment, final Attachment prevAttachment) {
-        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);
-            }
-            mThumbnailTask = new ThumbnailLoadTask(mIcon.getWidth(), mIcon.getHeight());
-            mThumbnailTask.execute(imageUri);
-        } else if (imageUri == null) {
-            // not an image, or no thumbnail exists. fall back to default.
-            // async image load must separately ensure the default appears upon load failure.
-            setThumbnailToDefault();
-        }
-    }
-
-    private void setThumbnailToDefault() {
+    public void setThumbnailToDefault() {
         mIcon.setImageResource(R.drawable.ic_menu_attachment_holo_light);
         mIcon.setScaleType(ImageView.ScaleType.CENTER);
     }
 
-    private class ThumbnailLoadTask extends AsyncTask<Uri, Void, Bitmap> {
+    public void setThumbnail(Bitmap result) {
+        mIcon.setImageBitmap(result);
+        mIcon.setScaleType(mIconScaleType);
+    }
 
-        private final int mWidth;
-        private final int mHeight;
+    public int getThumbnailWidth() {
+        return mIcon.getWidth();
+    }
 
-        public ThumbnailLoadTask(int width, int height) {
-            mWidth = width;
-            mHeight = height;
-        }
+    public int getThumbnailHeight() {
+        return mIcon.getHeight();
+    }
 
-        @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;
-
-                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);
-        }
-
+    @Override
+    public ContentResolver getResolver() {
+        return getContext().getContentResolver();
     }
 }
diff --git a/src/com/android/mail/ui/ThumbnailLoadTask.java b/src/com/android/mail/ui/ThumbnailLoadTask.java
new file mode 100644
index 0000000..d6cf55f
--- /dev/null
+++ b/src/com/android/mail/ui/ThumbnailLoadTask.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * 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.ui;
+
+import android.content.res.AssetFileDescriptor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+
+import com.android.mail.providers.Attachment;
+import com.android.mail.utils.LogUtils;
+
+import java.io.IOException;
+
+/**
+ * Performs the load of a thumbnail bitmap in a background
+ * {@link AsyncTask}. Available for use with any view that implements
+ * the {@link AttachmentBitmapHolder} interface.
+ */
+public class ThumbnailLoadTask extends AsyncTask<Uri, Void, Bitmap> {
+    private static final String LOG_TAG = new LogUtils().getLogTag();
+
+    private final AttachmentBitmapHolder mHolder;
+    private final int mWidth;
+    private final int mHeight;
+
+    public static void setupThumbnailPreview(
+            ThumbnailLoadTask task, AttachmentBitmapHolder holder,
+            Attachment attachment, final Attachment prevAttachment) {
+        if (attachment == null) {
+            holder.setThumbnailToDefault();
+            return;
+        }
+
+        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 (task != null) {
+                task.cancel(true);
+            }
+            task = new ThumbnailLoadTask(
+                    holder, holder.getThumbnailWidth(), holder.getThumbnailHeight());
+            task.execute(imageUri);
+        } else if (imageUri == null) {
+            // not an image, or no thumbnail exists. fall back to default.
+            // async image load must separately ensure the default appears upon load failure.
+            holder.setThumbnailToDefault();
+        }
+    }
+
+    public ThumbnailLoadTask(AttachmentBitmapHolder holder, int width, int height) {
+        mHolder = holder;
+        mWidth = width;
+        mHeight = height;
+    }
+
+    @Override
+    protected Bitmap doInBackground(Uri... params) {
+        final Uri thumbnailUri = params[0];
+
+        AssetFileDescriptor fd = null;
+        Bitmap result = null;
+
+        try {
+            fd = mHolder.getResolver().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;
+
+            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");
+            mHolder.setThumbnailToDefault();
+            return;
+        }
+
+        LogUtils.d(LOG_TAG, "back in UI thread, decode success, w/h=%d/%d", result.getWidth(),
+                result.getHeight());
+        mHolder.setThumbnail(result);
+    }
+
+}