bottom-aligned attachments

Refactor attachment view logic out of MessageHeaderView into
its own view class, paired with MessageFooterItem.

Change-Id: Ic6539f52d53be0e2d23144abec1baffb9368f8cd
diff --git a/res/layout/conversation_message_attachment.xml b/res/layout/conversation_message_attachment.xml
index db9463f..eb15614 100644
--- a/res/layout/conversation_message_attachment.xml
+++ b/res/layout/conversation_message_attachment.xml
@@ -18,7 +18,7 @@
 <com.android.mail.browse.MessageHeaderAttachment xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginTop="8dp"
+    android:layout_marginBottom="8dp"
     android:orientation="vertical"
     android:divider="?android:attr/dividerHorizontal"
     android:showDividers="middle"
diff --git a/res/layout/conversation_message_attachments.xml b/res/layout/conversation_message_footer.xml
similarity index 77%
rename from res/layout/conversation_message_attachments.xml
rename to res/layout/conversation_message_footer.xml
index 6bf6862..ad675d3 100644
--- a/res/layout/conversation_message_attachments.xml
+++ b/res/layout/conversation_message_footer.xml
@@ -15,12 +15,13 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.mail.browse.MessageFooterView
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/attachments"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical"
-    android:layout_marginTop="8dp"
-    android:layout_marginLeft="16dp"
-    android:layout_marginRight="16dp">
-</LinearLayout>
+    android:paddingLeft="16dp"
+    android:paddingRight="16dp"
+    android:paddingBottom="8dp">
+</com.android.mail.browse.MessageFooterView>
diff --git a/src/com/android/mail/browse/ConversationViewAdapter.java b/src/com/android/mail/browse/ConversationViewAdapter.java
index 75ed84d..f27ca27 100644
--- a/src/com/android/mail/browse/ConversationViewAdapter.java
+++ b/src/com/android/mail/browse/ConversationViewAdapter.java
@@ -164,7 +164,7 @@
         public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
             final MessageHeaderView v = (MessageHeaderView) inflater.inflate(
                     R.layout.conversation_message_header, parent, false);
-            v.initialize(mDateBuilder, mAccount, mLoaderManager, false /* defaultReplyAll */);
+            v.initialize(mDateBuilder, mAccount, false /* defaultReplyAll */);
             v.setCallbacks(mMessageCallbacks);
             return v;
         }
@@ -205,20 +205,21 @@
 
         @Override
         public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
-            // TODO
-            return new View(context);
+            final MessageFooterView v = (MessageFooterView) inflater.inflate(
+                    R.layout.conversation_message_footer, parent, false);
+            v.initialize(mLoaderManager);
+            return v;
         }
 
         @Override
         public void bindView(View v) {
-            final Message message = headerItem.message;
-            // TODO
+            final MessageFooterView attachmentsView = (MessageFooterView) v;
+            attachmentsView.bind(headerItem.message, headerItem.expanded);
         }
 
         @Override
         public int measureHeight(View v, ViewGroup parent) {
-            // TODO
-            return 0;
+            return Utils.measureViewHeight(v, parent);
         }
 
         @Override
diff --git a/src/com/android/mail/browse/MessageFooterView.java b/src/com/android/mail/browse/MessageFooterView.java
new file mode 100644
index 0000000..fa86e12
--- /dev/null
+++ b/src/com/android/mail/browse/MessageFooterView.java
@@ -0,0 +1,194 @@
+/*
+ * 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 com.google.common.collect.Lists;
+
+import android.app.LoaderManager;
+import android.content.Context;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+
+import com.android.mail.browse.AttachmentLoader.AttachmentCursor;
+import com.android.mail.browse.ConversationContainer.DetachListener;
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.Message;
+import com.android.mail.utils.LogUtils;
+
+import java.util.List;
+
+public class MessageFooterView extends LinearLayout implements DetachListener,
+        LoaderManager.LoaderCallbacks<Cursor> {
+
+    private Message mMessage;
+    private LoaderManager mLoaderManager;
+    private AttachmentCursor mAttachmentsCursor;
+    private boolean mIsExpanded;
+    private LayoutInflater mInflater;
+
+    /**
+     * An easy way for the conversation view to disable immediately kicking off attachment loaders
+     * when measuring overlays during the initial render.
+     */
+    private static boolean sEnableAttachmentLoaders = true;
+
+    private static final String LOG_TAG = new LogUtils().getLogTag();
+
+    public MessageFooterView(Context context) {
+        this(context, null);
+    }
+
+    public MessageFooterView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        mInflater = LayoutInflater.from(context);
+    }
+
+    /**
+     * Utility method that can be used to temporarily disable attachment loaders in all
+     * {@link MessageFooterView}s.
+     *
+     * @param enabled true to enable loaders
+     */
+    public static void enableAttachmentLoaders(boolean enabled) {
+        sEnableAttachmentLoaders = enabled;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+    }
+
+    public void initialize(LoaderManager loaderManager) {
+        mLoaderManager = loaderManager;
+    }
+
+    public void bind(Message msg, boolean expanded) {
+        mMessage = msg;
+        mIsExpanded = expanded;
+
+        removeAllViewsInLayout();
+
+        // kick off load of Attachment objects in background thread
+        final Integer attachmentLoaderId = getAttachmentLoaderId();
+        if (sEnableAttachmentLoaders && attachmentLoaderId != null) {
+            LogUtils.d(LOG_TAG, "binding footer view, calling initLoader for message %d",
+                    attachmentLoaderId);
+            mLoaderManager.initLoader(attachmentLoaderId, Bundle.EMPTY, this);
+        }
+
+        if (mIsExpanded) {
+            setVisibility(VISIBLE);
+            // Do an initial render if initLoader didn't already do one
+            if (getChildCount() == 0) {
+                renderAttachments();
+            }
+        } else {
+            setVisibility(GONE);
+        }
+    }
+
+    private void destroyLoader() {
+        final Integer loaderId = getAttachmentLoaderId();
+        if (mLoaderManager != null && loaderId != null) {
+            LogUtils.d(LOG_TAG, "detaching/reusing footer view,"
+                    + " calling destroyLoader for message %d", loaderId);
+            mLoaderManager.destroyLoader(loaderId);
+        }
+    }
+
+    private void renderAttachments() {
+        if (!mIsExpanded) {
+            return;
+        }
+
+        List<Attachment> attachments;
+        if (mAttachmentsCursor != null && !mAttachmentsCursor.isClosed()) {
+            int i = -1;
+            attachments = Lists.newArrayList();
+            while (mAttachmentsCursor.moveToPosition(++i)) {
+                attachments.add(mAttachmentsCursor.get());
+            }
+        } else {
+            // before the attachment loader results are in, we can still render immediately using
+            // the basic info in the message's attachmentsJSON
+            attachments = mMessage.getAttachments();
+        }
+        renderAttachments(attachments);
+    }
+
+    private void renderAttachments(List<Attachment> attachments) {
+        for (Attachment attachment : attachments) {
+            MessageHeaderAttachment attachView = (MessageHeaderAttachment) findViewWithTag(
+                    attachment.uri);
+
+            if (attachView == null) {
+                attachView = MessageHeaderAttachment.inflate(mInflater, this);
+                attachView.setTag(attachment.uri);
+                addView(attachView);
+            }
+
+            attachView.render(attachment);
+        }
+    }
+
+    private Integer getAttachmentLoaderId() {
+        Integer id = null;
+        if (mMessage != null && mMessage.hasAttachments && mMessage.attachmentListUri != null) {
+            id = mMessage.attachmentListUri.hashCode();
+        }
+        return id;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        destroyLoader();
+    }
+
+    @Override
+    public void onDetachedFromParent() {
+        destroyLoader();
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        return new AttachmentLoader(getContext(), mMessage.attachmentListUri);
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        mAttachmentsCursor = (AttachmentCursor) data;
+
+        if (mAttachmentsCursor == null || mAttachmentsCursor.isClosed()) {
+            return;
+        }
+
+        renderAttachments();
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        mAttachmentsCursor = null;
+    }
+
+}
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index 30c26d8..0e2cc5d 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -16,14 +16,10 @@
 
 package com.android.mail.browse;
 
-import android.app.LoaderManager;
 import android.content.AsyncQueryHandler;
 import android.content.Context;
-import android.content.Loader;
-import android.database.Cursor;
 import android.graphics.Canvas;
 import android.graphics.Typeface;
-import android.os.Bundle;
 import android.provider.ContactsContract;
 import android.text.Spannable;
 import android.text.SpannableStringBuilder;
@@ -47,27 +43,21 @@
 import com.android.mail.FormattedDateBuilder;
 import com.android.mail.R;
 import com.android.mail.SenderInfoLoader.ContactInfo;
-import com.android.mail.browse.AttachmentLoader.AttachmentCursor;
 import com.android.mail.compose.ComposeActivity;
 import com.android.mail.perf.Timer;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Address;
-import com.android.mail.providers.Attachment;
 import com.android.mail.providers.Message;
 import com.android.mail.providers.UIProvider;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
-
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
 
 import java.io.IOException;
 import java.io.StringReader;
-import java.util.List;
 
 public class MessageHeaderView extends LinearLayout implements OnClickListener,
-        OnMenuItemClickListener, HeaderBlock, LoaderManager.LoaderCallbacks<Cursor>,
-        ConversationContainer.DetachListener {
+        OnMenuItemClickListener, HeaderBlock {
 
     /**
      * Cap very long recipient lists during summary construction for efficiency.
@@ -101,7 +91,6 @@
     private ViewGroup mCollapsedDetailsView;
     private ViewGroup mExpandedDetailsView;
     private ViewGroup mImagePromptView;
-    private ViewGroup mAttachmentsView;
     private View mBottomBorderView;
     private ImageView mPresenceView;
 
@@ -144,11 +133,6 @@
 
     private int mDrawTranslateY;
 
-    /**
-     * List of attachments for this message, loaded asynchronously.
-     */
-    private AttachmentCursor mAttachments;
-
     private CharSequence mTimestampShort;
 
     /**
@@ -173,8 +157,6 @@
     private boolean mCollapsedDetailsValid;
     private boolean mExpandedDetailsValid;
 
-    private LoaderManager mLoaderManager;
-
     private final LayoutInflater mInflater;
 
     private AsyncQueryHandler mQueryHandler;
@@ -316,21 +298,12 @@
     }
 
     public void initialize(FormattedDateBuilder dateBuilder, Account account,
-            LoaderManager loaderManager, boolean defaultReplyAll) {
+            boolean defaultReplyAll) {
         mDateBuilder = dateBuilder;
         mAccount = account;
-        mLoaderManager = loaderManager;
         mDefaultReplyAll = defaultReplyAll;
     }
 
-    private Integer getAttachmentLoaderId() {
-        Integer id = null;
-        if (mMessage != null && mMessage.attachmentListUri != null) {
-            id = mMessage.attachmentListUri.hashCode();
-        }
-        return id;
-    }
-
     public void bind(Message message, boolean expanded, boolean showImagePrompt) {
         Timer t = new Timer();
         t.start(HEADER_RENDER_TAG);
@@ -352,16 +325,6 @@
         mBcc = mMessage.getBccAddresses();
         mReplyTo = mMessage.getReplyToAddresses();
 
-        if (mAttachmentsView != null) {
-            mAttachmentsView.removeAllViews();
-        }
-
-        // kick off load of Attachment objects in background thread
-        final Integer attachmentLoaderId = getAttachmentLoaderId();
-        if (mMessage.hasAttachments && attachmentLoaderId != null) {
-            mLoaderManager.initLoader(getAttachmentLoaderId(), Bundle.EMPTY, this);
-        }
-
         /**
          * Turns draft mode on or off. Draft mode hides message operations other
          * than "edit", hides contact photo, hides presence, and changes the
@@ -408,49 +371,6 @@
         t.pause(HEADER_RENDER_TAG);
     }
 
-    // Attachment list loader methods
-
-    @Override
-    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-        return new AttachmentLoader(getContext(), mMessage.attachmentListUri);
-    }
-
-    @Override
-    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-        mAttachments = (AttachmentCursor) data;
-
-        if (mAttachments == null || mAttachments.isClosed()) {
-            return;
-        }
-
-        renderAttachments(mAttachmentsView);
-    }
-
-    @Override
-    public void onLoaderReset(Loader<Cursor> loader) {
-        mAttachments = null;
-    }
-
-    private void destroyLoader() {
-        final Integer loaderId = getAttachmentLoaderId();
-        if (mLoaderManager != null && loaderId != null) {
-            LogUtils.d(LOG_TAG, "detaching header view, calling destroyLoader for message %d",
-                    loaderId);
-            mLoaderManager.destroyLoader(loaderId);
-        }
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        destroyLoader();
-    }
-
-    @Override
-    public void onDetachedFromParent() {
-        destroyLoader();
-    }
-
     private boolean isInOutbox() {
         // TODO: what should this read? Folder info?
         return false;
@@ -901,7 +821,8 @@
             hideCollapsedDetails();
             hideExpandedDetails();
             hideShowImagePrompt();
-            hideAttachments();
+            // FIXME: coordinate with matching footer (if exists) to show/hide
+            // hideAttachments();
         } else {
             setMessageDetailsExpanded(mDetailsExpanded);
             if (mShowImagePrompt) {
@@ -909,70 +830,20 @@
             } else {
                 hideShowImagePrompt();
             }
+            // FIXME: coordinate with matching footer (if exists) to show/hide
+            /*
             if (mMessage.hasAttachments) {
                 showAttachments();
             } else {
                 hideAttachments();
             }
+            */
         }
         if (mBottomBorderView != null) {
             mBottomBorderView.setVisibility(vis);
         }
     }
 
-    private void showAttachments() {
-        if (mAttachmentsView == null) {
-            ViewGroup container = (ViewGroup) mInflater.inflate(
-                    R.layout.conversation_message_attachments, this, false);
-
-            addView(container);
-            mAttachmentsView = container;
-        }
-        renderAttachments(mAttachmentsView);
-        mAttachmentsView.setVisibility(VISIBLE);
-    }
-
-    private void renderAttachments(ViewGroup container) {
-        if (container == null) {
-            return;
-        }
-
-        List<Attachment> attachments;
-        if (mAttachments != null && !mAttachments.isClosed()) {
-            int i = -1;
-            attachments = Lists.newArrayList();
-            while (mAttachments.moveToPosition(++i)) {
-                attachments.add(mAttachments.get());
-            }
-        } else {
-            // before the attachment loader results are in, we can still render immediately using
-            // the basic info in the message's attachmentsJSON
-            attachments = mMessage.getAttachments();
-        }
-        renderAttachments(attachments, container);
-    }
-
-    private void renderAttachments(List<Attachment> attachments, ViewGroup container) {
-        for (Attachment attachment : attachments) {
-            MessageHeaderAttachment attachView = (MessageHeaderAttachment)
-                    container.findViewWithTag(attachment.uri);
-
-            if (attachView == null) {
-                attachView = MessageHeaderAttachment.inflate(mInflater, container);
-                attachView.setTag(attachment.uri);
-                container.addView(attachView);
-            }
-
-            attachView.render(attachment);
-        }
-    }
-
-    private void hideAttachments() {
-        if (mAttachmentsView != null) {
-            mAttachmentsView.setVisibility(GONE);
-        }
-    }
-
     public void hideMessageDetails() {
         setMessageDetailsVisibility(GONE);
     }
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index b669c26..8448023 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -51,6 +51,7 @@
 import com.android.mail.browse.ConversationViewHeader;
 import com.android.mail.browse.ConversationWebView;
 import com.android.mail.browse.MessageCursor;
+import com.android.mail.browse.MessageFooterView;
 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Conversation;
@@ -333,6 +334,10 @@
 
         mAdapter.clear();
 
+        // We don't need to kick off attachment loaders during this first measurement phase,
+        // so disable them temporarily.
+        MessageFooterView.enableAttachmentLoaders(false);
+
         // N.B. the units of height for spacers are actually dp and not px because WebView assumes
         // a pixel is an mdpi pixel, unless you set device-dpi.
 
@@ -363,6 +368,9 @@
                     footerDp);
         }
 
+        // Re-enable attachment loaders
+        MessageFooterView.enableAttachmentLoaders(true);
+
         mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
 
         return mTemplates.endConversation(mBaseUri, 320);