Merge "Non-image attachments now display as a list." into jb-ub-mail
diff --git a/res/layout/conversation_message_attachment.xml b/res/layout/conversation_message_attachment.xml
deleted file mode 100644
index e6a8ad6..0000000
--- a/res/layout/conversation_message_attachment.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?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.
--->
-<com.android.mail.browse.MessageAttachmentTile
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/attachment_tile"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:layout_marginBottom="8dp"
-    android:orientation="vertical"
-    android:divider="?android:attr/dividerHorizontal"
-    android:showDividers="middle"
-    android:background="@drawable/attachment_bg_holo">
-
-    <RelativeLayout
-        android:id="@+id/attachment_tile_layout"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" >
-
-        <ImageView
-            android:id="@+id/attachment_tile_image"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:scaleType="centerCrop" />
-
-        <ProgressBar
-            android:id="@+id/attachment_progress"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentBottom="true"
-            style="?android:attr/progressBarStyleHorizontal"
-            android:indeterminate="false"
-            android:visibility="gone" />
-
-        <View
-            android:id="@+id/attachment_tile_push_state"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:focusable="true"
-            android:background="?android:attr/selectableItemBackground" />
-
-    </RelativeLayout>
-
-</com.android.mail.browse.MessageAttachmentTile>
diff --git a/res/layout/conversation_message_attachment_bar.xml b/res/layout/conversation_message_attachment_bar.xml
new file mode 100644
index 0000000..cda1ba9
--- /dev/null
+++ b/res/layout/conversation_message_attachment_bar.xml
@@ -0,0 +1,95 @@
+<?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.
+-->
+<com.android.mail.browse.MessageAttachmentBar
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginBottom="8dip"
+    android:rowCount="3"
+    android:columnCount="3"
+    android:background="@drawable/attachment_bg_holo">
+
+    <ImageView
+        android:id="@+id/attachment_icon"
+        android:layout_width="48dip"
+        android:layout_height="48dip"
+        android:layout_column="0"
+        android:layout_row="0"
+        android:layout_rowSpan="3"
+        android:scaleType="center"
+        android:src="@drawable/ic_menu_attachment_holo_light"
+        android:background="#e5e5e5" />
+
+    <TextView
+        android:id="@+id/attachment_title"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_gravity="fill_horizontal"
+        android:layout_marginLeft="8dip"
+        android:layout_column="1"
+        android:layout_row="0"
+        android:singleLine="true"
+        android:ellipsize="end" />
+
+    <TextView
+        android:id="@+id/attachment_subtitle"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_gravity="fill_horizontal"
+        android:layout_marginLeft="8dip"
+        android:layout_column="1"
+        android:layout_row="1"
+        android:singleLine="true"
+        android:ellipsize="end" />
+
+    <ProgressBar
+        android:id="@+id/attachment_progress"
+        android:layout_height="0dip"
+        android:layout_gravity="fill"
+        android:layout_column="1"
+        android:layout_row="2"
+        android:layout_columnSpan="2"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:indeterminate="false"
+        android:visibility="invisible" />
+
+    <ImageView
+        android:id="@+id/overflow"
+        android:layout_width="48dip"
+        android:layout_height="match_parent"
+        android:layout_column="2"
+        android:layout_row="0"
+        android:layout_rowSpan="3"
+        android:visibility="gone"
+        style="@android:style/Widget.Holo.Light.ActionButton.Overflow" />
+
+    <ImageView
+        android:id="@+id/cancel_attachment"
+        android:src="@drawable/ic_cancel_holo_light"
+        android:layout_width="24dip"
+        android:layout_height="24dip"
+        android:layout_marginTop="12dip"
+        android:layout_marginRight="12dip"
+        android:layout_marginBottom="12dip"
+        android:layout_marginLeft="12dip"
+        android:layout_column="2"
+        android:layout_row="0"
+        android:layout_rowSpan="3"
+        android:visibility="gone"/>
+
+</com.android.mail.browse.MessageAttachmentBar>
diff --git a/res/layout/conversation_message_attachment_tile.xml b/res/layout/conversation_message_attachment_tile.xml
new file mode 100644
index 0000000..051a038
--- /dev/null
+++ b/res/layout/conversation_message_attachment_tile.xml
@@ -0,0 +1,38 @@
+<?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.
+-->
+<com.android.mail.browse.MessageAttachmentTile
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/attachment_tile"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@drawable/attachment_bg_holo">
+
+    <ImageView
+        android:id="@+id/attachment_tile_image"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop" />
+
+    <View
+        android:id="@+id/attachment_tile_push_state"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:focusable="true"
+        android:background="?android:attr/selectableItemBackground" />
+
+</com.android.mail.browse.MessageAttachmentTile>
diff --git a/res/layout/conversation_message_footer.xml b/res/layout/conversation_message_footer.xml
index 6b7627a..7ceedc7 100644
--- a/res/layout/conversation_message_footer.xml
+++ b/res/layout/conversation_message_footer.xml
@@ -45,4 +45,12 @@
         android:visibility="gone"
         android:layout_marginLeft="16dp"
         android:layout_marginRight="16dp"/>
+    <LinearLayout
+        android:id="@+id/attachment_bar_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:layout_marginLeft="16dp"
+        android:layout_marginRight="16dp"
+        android:orientation="vertical"/>
 </com.android.mail.browse.MessageFooterView>
diff --git a/res/menu/message_footer_overflow_menu.xml b/res/menu/message_footer_overflow_menu.xml
new file mode 100644
index 0000000..9de486c
--- /dev/null
+++ b/res/menu/message_footer_overflow_menu.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/preview_attachment"
+          android:title="@string/preview_attachment" />
+    <item android:id="@+id/save_attachment"
+          android:title="@string/save_attachment" />
+</menu>
diff --git a/src/com/android/mail/browse/AttachmentActionHandler.java b/src/com/android/mail/browse/AttachmentActionHandler.java
new file mode 100644
index 0000000..5fbea62
--- /dev/null
+++ b/src/com/android/mail/browse/AttachmentActionHandler.java
@@ -0,0 +1,128 @@
+package com.android.mail.browse;
+
+import android.app.ProgressDialog;
+import android.content.AsyncQueryHandler;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.Uri;
+
+import com.android.mail.R;
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.UIProvider.AttachmentColumns;
+import com.android.mail.providers.UIProvider.AttachmentState;
+
+public class AttachmentActionHandler implements DialogInterface.OnCancelListener,
+        DialogInterface.OnDismissListener {
+    private ProgressDialog mViewProgressDialog;
+    private Attachment mAttachment;
+
+    private final AttachmentCommandHandler mCommandHandler;
+    private final AttachmentViewInterface mView;
+    private final Context mContext;
+
+    private class AttachmentCommandHandler extends AsyncQueryHandler {
+
+        public AttachmentCommandHandler(Context context) {
+            super(context.getContentResolver());
+        }
+
+        /**
+         * Asynchronously begin an update() on a ContentProvider.
+         *
+         */
+        public void sendCommand(Uri uri, ContentValues params) {
+            startUpdate(0, null, uri, params, null, null);
+        }
+
+    }
+
+    public AttachmentActionHandler(Context context, AttachmentViewInterface view) {
+        mCommandHandler = new AttachmentCommandHandler(context);
+        mView = view;
+        mContext = context;
+    }
+
+    public void setAttachment(Attachment attachment) {
+        mAttachment = attachment;
+    }
+
+    public void showAttachment(int destination) {
+        if (mAttachment.isPresentLocally()) {
+            mView.viewAttachment();
+        } else {
+            showDownloadingDialog();
+            startDownloadingAttachment(destination);
+        }
+    }
+
+    public void startDownloadingAttachment(int destination) {
+        final ContentValues params = new ContentValues(2);
+        params.put(AttachmentColumns.STATE, AttachmentState.DOWNLOADING);
+        params.put(AttachmentColumns.DESTINATION, destination);
+
+        mCommandHandler.sendCommand(mAttachment.uri, params);
+    }
+
+    public void cancelAttachment() {
+        final ContentValues params = new ContentValues(1);
+        params.put(AttachmentColumns.STATE, AttachmentState.NOT_SAVED);
+
+        mCommandHandler.sendCommand(mAttachment.uri, params);
+    }
+
+    /**
+     * Displays a loading dialog to be used for downloading attachments.
+     * Must be called on the UI thread.
+     */
+    private void showDownloadingDialog() {
+        mViewProgressDialog = new ProgressDialog(mContext);
+        mViewProgressDialog.setTitle(R.string.fetching_attachment);
+        mViewProgressDialog.setMessage(mContext.getResources().getString(R.string.please_wait));
+        mViewProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+        mViewProgressDialog.setMax(mAttachment.size);
+        mViewProgressDialog.setOnDismissListener(this);
+        mViewProgressDialog.setOnCancelListener(this);
+        mViewProgressDialog.show();
+
+        // The progress number format needs to be set after the dialog is shown.  See bug: 5149918
+        mViewProgressDialog.setProgressNumberFormat(null);
+    }
+
+    @Override
+    public void onDismiss(DialogInterface dialog) {
+        mViewProgressDialog = null;
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        cancelAttachment();
+    }
+
+    /**
+     * 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.
+     */
+    public void updateStatus() {
+        final boolean showProgress = mAttachment.size > 0 && mAttachment.downloadedSize > 0
+                && mAttachment.downloadedSize < mAttachment.size;
+
+        if (mViewProgressDialog != null && mViewProgressDialog.isShowing()) {
+            mViewProgressDialog.setProgress(mAttachment.downloadedSize);
+            mViewProgressDialog.setIndeterminate(!showProgress);
+
+            if (!mAttachment.isDownloading()) {
+                mViewProgressDialog.dismiss();
+            }
+
+            if (mAttachment.state == AttachmentState.SAVED) {
+                mView.viewAttachment();
+            }
+        } else {
+            mView.updateProgress(showProgress);
+        }
+
+        // Call update status for the view so that it can do some specific things.
+        mView.updateStatus();
+    }
+}
diff --git a/src/com/android/mail/browse/AttachmentViewInterface.java b/src/com/android/mail/browse/AttachmentViewInterface.java
new file mode 100644
index 0000000..8e89fd7
--- /dev/null
+++ b/src/com/android/mail/browse/AttachmentViewInterface.java
@@ -0,0 +1,24 @@
+package com.android.mail.browse;
+
+public interface AttachmentViewInterface {
+
+    /**
+     * View an attachment. The different attachment types handle this
+     * action differently and so each view handles it in their
+     * own manner.
+     */
+    public void viewAttachment();
+
+    /**
+     * Allows the view to know when it should update its progress.
+     * @param showProgress true if the the view should show a determinate
+     * progress value
+     */
+    public void updateProgress(boolean showDeterminateProgress);
+
+    /**
+     * Allows the view to do some view-specific status updating.
+     * Called in {@link AttachmentActionHandler#updateStatus}.
+     */
+    public void updateStatus();
+}
diff --git a/src/com/android/mail/browse/MessageAttachmentBar.java b/src/com/android/mail/browse/MessageAttachmentBar.java
new file mode 100644
index 0000000..ec7101b
--- /dev/null
+++ b/src/com/android/mail/browse/MessageAttachmentBar.java
@@ -0,0 +1,296 @@
+/*
+ * 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.browse;
+
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.GridLayout;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.mail.R;
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.UIProvider.AttachmentDestination;
+import com.android.mail.providers.UIProvider.AttachmentState;
+import com.android.mail.utils.AttachmentUtils;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.MimeType;
+import com.android.mail.utils.Utils;
+
+/**
+ * View for a single attachment in conversation view. Shows download status and allows launching
+ * intents to act on an attachment.
+ *
+ */
+public class MessageAttachmentBar extends GridLayout implements OnClickListener,
+        OnMenuItemClickListener, AttachmentViewInterface {
+
+    private Attachment mAttachment;
+    private TextView mTitle;
+    private TextView mSubTitle;
+    private String mAttachmentSizeText;
+    private String mDisplayType;
+    private ProgressBar mProgress;
+    private ImageView mCancelButton;
+    private PopupMenu mPopup;
+    private ImageView mOverflowButton;
+
+    private final AttachmentActionHandler mActionHandler;
+
+    private static final String LOG_TAG = new LogUtils().getLogTag();
+
+    public MessageAttachmentBar(Context context) {
+        this(context, null);
+    }
+
+    public MessageAttachmentBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mActionHandler = new AttachmentActionHandler(context, this);
+    }
+
+    public static MessageAttachmentBar inflate(LayoutInflater inflater, ViewGroup parent) {
+        MessageAttachmentBar view = (MessageAttachmentBar) inflater.inflate(
+                R.layout.conversation_message_attachment_bar, parent, false);
+        return view;
+    }
+
+    /**
+     * Render or update an attachment's view. This happens immediately upon instantiation, and
+     * repeatedly as status updates stream in, so only properties with new or changed values will
+     * cause sub-views to update.
+     *
+     */
+    public void render(Attachment attachment) {
+        final Attachment prevAttachment = mAttachment;
+        mAttachment = attachment;
+        mActionHandler.setAttachment(mAttachment);
+
+        LogUtils.d(LOG_TAG, "got attachment list row: name=%s state/dest=%d/%d dled=%d" +
+                " contentUri=%s MIME=%s", attachment.name, attachment.state,
+                attachment.destination, attachment.downloadedSize, attachment.contentUri,
+                attachment.contentType);
+
+        if (prevAttachment == null || TextUtils.equals(attachment.name, prevAttachment.name)) {
+            mTitle.setText(attachment.name);
+        }
+
+        if (prevAttachment == null || attachment.size != prevAttachment.size) {
+            mAttachmentSizeText = AttachmentUtils.convertToHumanReadableSize(getContext(),
+                    attachment.size);
+            mDisplayType = AttachmentUtils.getDisplayType(getContext(), attachment);
+            updateSubtitleText(null);
+        }
+
+        mProgress.setMax(attachment.size);
+
+        updateActions();
+        mActionHandler.updateStatus();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mTitle = (TextView) findViewById(R.id.attachment_title);
+        mSubTitle = (TextView) findViewById(R.id.attachment_subtitle);
+        mProgress = (ProgressBar) findViewById(R.id.attachment_progress);
+        mOverflowButton = (ImageView) findViewById(R.id.overflow);
+        mCancelButton = (ImageView) findViewById(R.id.cancel_attachment);
+
+        setOnClickListener(this);
+        mOverflowButton.setOnClickListener(this);
+        mCancelButton.setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View v) {
+        onClick(v.getId(), v);
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        mPopup.dismiss();
+        return onClick(item.getItemId(), null);
+    }
+
+    private boolean onClick(int res, View v) {
+        switch (res) {
+            case R.id.preview_attachment:
+                previewAttachment();
+                break;
+            case R.id.save_attachment:
+                if (mAttachment.canSave()) {
+                    mActionHandler.startDownloadingAttachment(AttachmentDestination.EXTERNAL);
+                }
+                break;
+            case R.id.cancel_attachment:
+                mActionHandler.cancelAttachment();
+                break;
+            case R.id.overflow: {
+                final boolean canSave = mAttachment.canSave() && !mAttachment.isDownloading();
+                final boolean canPreview = (mAttachment.previewIntent != null);
+
+                // If no overflow items are visible, just bail out.
+                // We shouldn't be able to get here anyhow since the overflow
+                // button should be hidden.
+                if (!canSave && !canPreview) {
+                    break;
+                }
+
+                if (mPopup == null) {
+                    mPopup = new PopupMenu(getContext(), v);
+                    mPopup.getMenuInflater().inflate(R.menu.message_footer_overflow_menu,
+                            mPopup.getMenu());
+                    mPopup.setOnMenuItemClickListener(this);
+                }
+
+                final Menu menu = mPopup.getMenu();
+                menu.findItem(R.id.preview_attachment).setVisible(canPreview);
+                menu.findItem(R.id.save_attachment).setVisible(canSave);
+
+                mPopup.show();
+                break;
+            }
+            default:
+                // Handles clicking the attachment
+                // in any area that is not the overflow
+                // button or cancel button or one of the
+                // overflow items.
+
+                // If we can install, install.
+                if (MimeType.isInstallable(mAttachment.contentType)) {
+                    mActionHandler.showAttachment(AttachmentDestination.EXTERNAL);
+                }
+                // If we can view or play with an on-device app,
+                // view or play.
+                else if (MimeType.isViewable(getContext(), mAttachment.contentType)
+                        || MimeType.isPlayable(mAttachment.contentType)) {
+                    mActionHandler.showAttachment(AttachmentDestination.CACHE);
+                }
+                // If we can only preview the attachment, preview.
+                else if (mAttachment.previewIntent != null) {
+                    previewAttachment();
+                }
+                // Otherwise, if we cannot do anything, show the info dialog.
+                else {
+                    AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+                    int dialogMessage = MimeType.isBlocked(mAttachment.contentType)
+                            ? R.string.attachment_type_blocked : R.string.no_application_found;
+                    builder.setTitle(R.string.more_info_attachment)
+                           .setMessage(dialogMessage)
+                           .show();
+                }
+                break;
+        }
+
+        return true;
+    }
+
+    public void viewAttachment() {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+                | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+        Utils.setIntentDataAndTypeAndNormalize(intent, mAttachment.contentUri,
+                mAttachment.contentType);
+        try {
+            getContext().startActivity(intent);
+        } catch (ActivityNotFoundException e) {
+            // couldn't find activity for View intent
+            LogUtils.e(LOG_TAG, "Coun't find Activity for intent", e);
+        }
+    }
+
+    private void previewAttachment() {
+        getContext().startActivity(mAttachment.previewIntent);
+    }
+
+    private void setButtonVisible(View button, boolean visible) {
+        button.setVisibility(visible ? VISIBLE : GONE);
+    }
+
+    /**
+     * Update all actions based on current downloading state.
+     */
+    private void updateActions() {
+        // To avoid visibility state transition bugs, every button's visibility should be touched
+        // once by this routine.
+
+        final boolean isDownloading = mAttachment.isDownloading();
+        final boolean canSave = mAttachment.canSave();
+        final boolean canPreview = (mAttachment.previewIntent != null);
+
+        setButtonVisible(mCancelButton, isDownloading);
+        setButtonVisible(mOverflowButton, !isDownloading && (canSave || canPreview));
+    }
+
+    public void updateStatus() {
+        if (mAttachment.state == AttachmentState.FAILED) {
+            mSubTitle.setText(getResources().getString(R.string.download_failed));
+        } else {
+            updateSubtitleText(mAttachment.isSavedToExternal() ?
+                    getResources().getString(R.string.saved) : null);
+        }
+    }
+
+    public void updateProgress(boolean showProgress) {
+        if (mAttachment.isDownloading()) {
+            mProgress.setProgress(mAttachment.downloadedSize);
+            setProgressVisible(true);
+            mProgress.setIndeterminate(!showProgress);
+        } else {
+            setProgressVisible(false);
+        }
+    }
+
+    private void setProgressVisible(boolean visible) {
+        if (visible) {
+            mProgress.setVisibility(VISIBLE);
+            mSubTitle.setVisibility(INVISIBLE);
+        } else {
+            mProgress.setVisibility(INVISIBLE);
+            mSubTitle.setVisibility(VISIBLE);
+        }
+    }
+
+    private void updateSubtitleText(String prefix) {
+        // TODO: make this a formatted resource when we have a UX design.
+        // not worth translation right now.
+        StringBuilder sb = new StringBuilder();
+        if (prefix != null) {
+            sb.append(prefix);
+        }
+        sb.append(mAttachmentSizeText);
+        sb.append(' ');
+        sb.append(mDisplayType);
+        mSubTitle.setText(sb.toString());
+    }
+}
diff --git a/src/com/android/mail/browse/MessageAttachmentTile.java b/src/com/android/mail/browse/MessageAttachmentTile.java
index 5b55b64..56acf20 100644
--- a/src/com/android/mail/browse/MessageAttachmentTile.java
+++ b/src/com/android/mail/browse/MessageAttachmentTile.java
@@ -17,13 +17,8 @@
 
 package com.android.mail.browse;
 
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
 import android.content.ActivityNotFoundException;
-import android.content.AsyncQueryHandler;
-import android.content.ContentValues;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.res.AssetFileDescriptor;
 import android.graphics.Bitmap;
@@ -37,20 +32,16 @@
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.widget.ImageView;
-import android.widget.LinearLayout;
 import android.widget.PopupMenu.OnMenuItemClickListener;
-import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
 
 import com.android.mail.R;
 import com.android.mail.photo.Intents;
 import com.android.mail.photo.Intents.PhotoViewIntentBuilder;
 import com.android.mail.photo.util.ImageUtils;
 import com.android.mail.providers.Attachment;
-import com.android.mail.providers.UIProvider.AttachmentColumns;
 import com.android.mail.providers.UIProvider.AttachmentDestination;
-import com.android.mail.providers.UIProvider.AttachmentState;
 import com.android.mail.utils.LogUtils;
-import com.android.mail.utils.MimeType;
 import com.android.mail.utils.Utils;
 
 import java.io.IOException;
@@ -60,39 +51,21 @@
  * intents to act on an attachment.
  *
  */
-public class MessageAttachmentTile extends LinearLayout implements OnClickListener,
-        OnMenuItemClickListener, DialogInterface.OnCancelListener,
-        DialogInterface.OnDismissListener {
+public class MessageAttachmentTile extends RelativeLayout implements OnClickListener,
+        OnMenuItemClickListener, AttachmentViewInterface {
 
     private Attachment mAttachment;
     private ImageView mIcon;
     private ImageView.ScaleType mIconScaleType;
     private int mPhotoIndex;
     private Uri mAttachmentsListUri;
-    private ProgressDialog mViewProgressDialog;
-    private AttachmentCommandHandler mCommandHandler;
-    private ProgressBar mProgress;
+
+    private final AttachmentActionHandler mActionHandler;
 
     private ThumbnailLoadTask mThumbnailTask;
 
     private static final String LOG_TAG = new LogUtils().getLogTag();
 
-    private class AttachmentCommandHandler extends AsyncQueryHandler {
-
-        public AttachmentCommandHandler() {
-            super(getContext().getContentResolver());
-        }
-
-        /**
-         * Asynchronously begin an update() on a ContentProvider.
-         *
-         */
-        public void sendCommand(ContentValues params) {
-            startUpdate(0, null, mAttachment.uri, params, null, null);
-        }
-
-    }
-
     private class ThumbnailLoadTask extends AsyncTask<Uri, Void, Bitmap> {
 
         private final int mWidth;
@@ -163,18 +136,18 @@
     }
 
     public MessageAttachmentTile(Context context) {
-        super(context);
+        this(context, null);
     }
 
     public MessageAttachmentTile(Context context, AttributeSet attrs) {
         super(context, attrs);
 
-        mCommandHandler = new AttachmentCommandHandler();
+        mActionHandler = new AttachmentActionHandler(context, this);
     }
 
     public static MessageAttachmentTile inflate(LayoutInflater inflater, ViewGroup parent) {
         MessageAttachmentTile view = (MessageAttachmentTile) inflater.inflate(
-                R.layout.conversation_message_attachment, parent, false);
+                R.layout.conversation_message_attachment_tile, parent, false);
         return view;
     }
 
@@ -192,6 +165,7 @@
 
         final Attachment prevAttachment = mAttachment;
         mAttachment = attachment;
+        mActionHandler.setAttachment(mAttachment);
         mAttachmentsListUri = attachmentsListUri;
         mPhotoIndex = index;
 
@@ -217,11 +191,7 @@
             setThumbnailToDefault();
         }
 
-        if (mProgress != null) {
-            mProgress.setMax(attachment.size);
-        }
-
-        updateStatus();
+        mActionHandler.updateStatus();
     }
 
     private void setThumbnailToDefault() {
@@ -229,52 +199,13 @@
         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.
-     */
-    private void updateStatus() {
-        final boolean showProgress = mAttachment.size > 0 && mAttachment.downloadedSize > 0
-                && mAttachment.downloadedSize < mAttachment.size;
 
-        if (mViewProgressDialog != null && mViewProgressDialog.isShowing()) {
-            mViewProgressDialog.setProgress(mAttachment.downloadedSize);
-            mViewProgressDialog.setIndeterminate(!showProgress);
-
-            if (!mAttachment.isDownloading()) {
-                mViewProgressDialog.dismiss();
-            }
-
-            if (mAttachment.state == AttachmentState.SAVED) {
-                sendViewIntent();
-            }
-        } else {
-
-            if (mAttachment.isDownloading()) {
-                mProgress.setProgress(mAttachment.downloadedSize);
-                setProgressVisible(true);
-                mProgress.setIndeterminate(!showProgress);
-            } else {
-                setProgressVisible(false);
-            }
-
-        }
-    }
-
-    private void setProgressVisible(boolean visible) {
-        if (visible) {
-            mProgress.setVisibility(VISIBLE);
-        } else {
-            mProgress.setVisibility(GONE);
-        }
-    }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
         mIcon = (ImageView) findViewById(R.id.attachment_tile_image);
-        mProgress = (ProgressBar) findViewById(R.id.attachment_progress);
 
         setOnClickListener(this);
 
@@ -292,66 +223,12 @@
     }
 
     private boolean onClick(int res, View v) {
-        switch (res) {
-            case R.id.preview_attachment:
-                getContext().startActivity(mAttachment.previewIntent);
-                break;
-            case R.id.view_attachment:
-            case R.id.play_attachment:
-            case R.id.attachment_tile:
-                showAttachment(AttachmentDestination.CACHE);
-                break;
-            case R.id.save_attachment:
-                if (mAttachment.canSave()) {
-                    startDownloadingAttachment(AttachmentDestination.EXTERNAL);
-                }
-                break;
-            case R.id.info_attachment:
-                AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
-                int dialogMessage = MimeType.isBlocked(mAttachment.contentType)
-                        ? R.string.attachment_type_blocked : R.string.no_application_found;
-                builder.setTitle(R.string.more_info_attachment).setMessage(dialogMessage).show();
-                break;
-            case R.id.install_attachment:
-                showAttachment(AttachmentDestination.EXTERNAL);
-                break;
-            case R.id.cancel_attachment:
-                cancelAttachment();
-                break;
-            default:
-                break;
-        }
+        mActionHandler.showAttachment(AttachmentDestination.CACHE);
+
         return true;
     }
 
-    private void showAttachment(int destination) {
-        if (mAttachment.isPresentLocally()) {
-            sendViewIntent();
-        } else {
-            showDownloadingDialog();
-            startDownloadingAttachment(destination);
-        }
-    }
-
-    private void startDownloadingAttachment(int destination) {
-        final ContentValues params = new ContentValues(2);
-        params.put(AttachmentColumns.STATE, AttachmentState.DOWNLOADING);
-        params.put(AttachmentColumns.DESTINATION, destination);
-
-        mCommandHandler.sendCommand(params);
-    }
-
-    private void cancelAttachment() {
-        final ContentValues params = new ContentValues(1);
-        params.put(AttachmentColumns.STATE, AttachmentState.NOT_SAVED);
-
-        mCommandHandler.sendCommand(params);
-    }
-
-    /**
-     * View an attachment by an application on device.
-     */
-    private void sendViewIntent() {
+    public void viewAttachment() {
         if (ImageUtils.isImageMimeType(Utils.normalizeMimeType(mAttachment.contentType))) {
             final PhotoViewIntentBuilder builder =
                     Intents.newPhotoViewActivityIntentBuilder(getContext());
@@ -376,32 +253,9 @@
         }
     }
 
-    /**
-     * Displays a loading dialog to be used for downloading attachments.
-     * Must be called on the UI thread.
-     */
-    private void showDownloadingDialog() {
-        mViewProgressDialog = new ProgressDialog(getContext());
-        mViewProgressDialog.setTitle(R.string.fetching_attachment);
-        mViewProgressDialog.setMessage(getResources().getString(R.string.please_wait));
-        mViewProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
-        mViewProgressDialog.setMax(mAttachment.size);
-        mViewProgressDialog.setOnDismissListener(this);
-        mViewProgressDialog.setOnCancelListener(this);
-        mViewProgressDialog.show();
-
-        // The progress number format needs to be set after the dialog is shown.  See bug: 5149918
-        mViewProgressDialog.setProgressNumberFormat(null);
+    public void updateProgress(boolean showDeterminateProgress) {
     }
 
-    @Override
-    public void onDismiss(DialogInterface dialog) {
-        mViewProgressDialog = null;
+    public void updateStatus() {
     }
-
-    @Override
-    public void onCancel(DialogInterface dialog) {
-        cancelAttachment();
-    }
-
 }
diff --git a/src/com/android/mail/browse/MessageFooterView.java b/src/com/android/mail/browse/MessageFooterView.java
index de9af60..4262c4c 100644
--- a/src/com/android/mail/browse/MessageFooterView.java
+++ b/src/com/android/mail/browse/MessageFooterView.java
@@ -23,6 +23,7 @@
 import android.database.Cursor;
 import android.os.Bundle;
 import android.util.AttributeSet;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
@@ -31,11 +32,13 @@
 import com.android.mail.browse.AttachmentLoader.AttachmentCursor;
 import com.android.mail.browse.ConversationContainer.DetachListener;
 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
+import com.android.mail.photo.util.ImageUtils;
 import com.android.mail.providers.Attachment;
 import com.android.mail.providers.Message;
 import com.android.mail.utils.LogUtils;
 import com.google.common.collect.Lists;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public class MessageFooterView extends LinearLayout implements DetachListener,
@@ -47,6 +50,8 @@
     private TextView mTitleText;
     private View mTitleBar;
     private AttachmentTileGrid mAttachmentGrid;
+    private LinearLayout mAttachmentBarList;
+    private final LayoutInflater mInflater;
 
     /**
      * An easy way for the conversation view to disable immediately kicking off attachment loaders
@@ -62,6 +67,8 @@
 
     public MessageFooterView(Context context, AttributeSet attrs) {
         super(context, attrs);
+
+        mInflater = LayoutInflater.from(context);
     }
 
     /**
@@ -81,6 +88,7 @@
         mTitleText = (TextView) findViewById(R.id.attachments_header_text);
         mTitleBar = findViewById(R.id.attachments_header_bar);
         mAttachmentGrid = (AttachmentTileGrid) findViewById(R.id.attachment_tile_grid);
+        mAttachmentBarList = (LinearLayout) findViewById(R.id.attachment_bar_list);
     }
 
     public void initialize(LoaderManager loaderManager) {
@@ -96,8 +104,11 @@
          */
 
         mAttachmentGrid.removeAllViewsInLayout();
+        mAttachmentBarList.removeAllViewsInLayout();
         mTitleText.setVisibility(View.GONE);
         mTitleBar.setVisibility(View.GONE);
+        mAttachmentGrid.setVisibility(View.GONE);
+        mAttachmentBarList.setVisibility(View.GONE);
 
         // kick off load of Attachment objects in background thread
         final Integer attachmentLoaderId = getAttachmentLoaderId();
@@ -108,7 +119,8 @@
         }
 
         // Do an initial render if initLoader didn't already do one
-        if (mAttachmentGrid.getChildCount() == 0) {
+        if (mAttachmentGrid.getChildCount() == 0 &&
+                mAttachmentBarList.getChildCount() == 0) {
             renderAttachments();
         }
         setVisibility(mMessageHeaderItem.isExpanded() ? VISIBLE : GONE);
@@ -144,12 +156,53 @@
             return;
         }
 
+        // filter the attachments into tiled and non-tiled
+        final int maxSize = attachments.size();
+        List<Attachment> tiledAttachments = new ArrayList<Attachment>(maxSize);
+        List<Attachment> barAttachments = new ArrayList<Attachment>(maxSize);
+
+        for (Attachment attachment : attachments) {
+            if (isTiledAttachment(attachment)) {
+                tiledAttachments.add(attachment);
+            } else {
+                barAttachments.add(attachment);
+            }
+        }
+
         mTitleText.setVisibility(View.VISIBLE);
         mTitleBar.setVisibility(View.VISIBLE);
+
+        renderTiledAttachments(tiledAttachments);
+        renderBarAttachments(barAttachments);
+    }
+
+    private void renderTiledAttachments(List<Attachment> tiledAttachments) {
         mAttachmentGrid.setVisibility(View.VISIBLE);
 
         // Setup the tiles.
-        mAttachmentGrid.configureGrid(mMessageHeaderItem.message.attachmentListUri, attachments);
+        mAttachmentGrid.configureGrid(
+                mMessageHeaderItem.message.attachmentListUri, tiledAttachments);
+    }
+
+    private void renderBarAttachments(List<Attachment> barAttachments) {
+        mAttachmentBarList.setVisibility(View.VISIBLE);
+
+        for (Attachment attachment : barAttachments) {
+            MessageAttachmentBar barAttachmentView =
+                    (MessageAttachmentBar) mAttachmentBarList.findViewWithTag(attachment.uri);
+
+            if (barAttachmentView == null) {
+                barAttachmentView = MessageAttachmentBar.inflate(mInflater, this);
+                barAttachmentView.setTag(attachment.uri);
+                mAttachmentBarList.addView(barAttachmentView);
+            }
+
+            barAttachmentView.render(attachment);
+        }
+    }
+
+    private boolean isTiledAttachment(final Attachment attachment) {
+        return ImageUtils.isImageMimeType(attachment.contentType);
     }
 
     private Integer getAttachmentLoaderId() {