Conversation UI Visual Refresh: border fixes.

Borders now expand and collapsed based upon the state
of the messages above and below them. Also fixed a bug
where replying to a message added an additional border.

Change-Id: Ia90caf4ea595767a90213fe33b29e1bd75c0aca0
diff --git a/assets/script.js b/assets/script.js
index be22529..b30305a 100644
--- a/assets/script.js
+++ b/assets/script.js
@@ -638,7 +638,8 @@
     measurePositions();
 }
 
-function setMessageBodyVisible(messageDomId, isVisible, spacerHeight) {
+function setMessageBodyVisible(messageDomId, isVisible, spacerHeight,
+        topBorderHeight, bottomBorderHeight) {
     var i, len;
     var visibility = isVisible ? "block" : "none";
     var messageDiv = document.querySelector("#" + messageDomId);
@@ -647,6 +648,27 @@
         console.log("can't set body visibility for message with id: " + messageDomId);
         return;
     }
+
+    // if the top border has changed, update the height of its spacer
+    if (topBorderHeight > 0) {
+        var border = messageDiv.previousElementSibling;
+        if (!border) {
+            console.log("can't set spacer for top border");
+            return;
+        }
+        border.style.height = topBorderHeight + "px";
+    }
+
+    // if the bottom border has changed, update the height of its spacer
+    if (bottomBorderHeight > 0) {
+        var border = messageDiv.nextElementSibling;
+        if (!border) {
+            console.log("can't set spacer for bottom border");
+            return;
+        }
+        border.style.height = bottomBorderHeight + "px";
+    }
+
     messageDiv.classList.toggle("expanded");
     for (i = 0, len = collapsibleDivs.length; i < len; i++) {
         collapsibleDivs[i].style.display = visibility;
diff --git a/res/drawable-hdpi/card_top.9.png b/res/drawable-hdpi/card_top.9.png
index 4162bdb..3008de8 100644
--- a/res/drawable-hdpi/card_top.9.png
+++ b/res/drawable-hdpi/card_top.9.png
Binary files differ
diff --git a/res/drawable-hdpi/snap_header_gradient.9.png b/res/drawable-hdpi/snap_header_gradient.9.png
index 7f59fd1..08d4ff1 100644
--- a/res/drawable-hdpi/snap_header_gradient.9.png
+++ b/res/drawable-hdpi/snap_header_gradient.9.png
Binary files differ
diff --git a/res/drawable-hdpi/stacked_message_gradient.9.png b/res/drawable-hdpi/stacked_message_gradient.9.png
new file mode 100644
index 0000000..7f59fd1
--- /dev/null
+++ b/res/drawable-hdpi/stacked_message_gradient.9.png
Binary files differ
diff --git a/res/drawable-mdpi/card_top.9.png b/res/drawable-mdpi/card_top.9.png
index ae7f41a..bfa86aa 100644
--- a/res/drawable-mdpi/card_top.9.png
+++ b/res/drawable-mdpi/card_top.9.png
Binary files differ
diff --git a/res/drawable-mdpi/snap_header_gradient.9.png b/res/drawable-mdpi/snap_header_gradient.9.png
index 083a660..f847fb7 100644
--- a/res/drawable-mdpi/snap_header_gradient.9.png
+++ b/res/drawable-mdpi/snap_header_gradient.9.png
Binary files differ
diff --git a/res/drawable-mdpi/stacked_message_gradient.9.png b/res/drawable-mdpi/stacked_message_gradient.9.png
new file mode 100644
index 0000000..083a660
--- /dev/null
+++ b/res/drawable-mdpi/stacked_message_gradient.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/card_top.9.png b/res/drawable-xhdpi/card_top.9.png
index 06b3714..2be4cde 100644
--- a/res/drawable-xhdpi/card_top.9.png
+++ b/res/drawable-xhdpi/card_top.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/snap_header_gradient.9.png b/res/drawable-xhdpi/snap_header_gradient.9.png
index dd56f38..05a2af8 100644
--- a/res/drawable-xhdpi/snap_header_gradient.9.png
+++ b/res/drawable-xhdpi/snap_header_gradient.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/stacked_message_gradient.9.png b/res/drawable-xhdpi/stacked_message_gradient.9.png
new file mode 100644
index 0000000..dd56f38
--- /dev/null
+++ b/res/drawable-xhdpi/stacked_message_gradient.9.png
Binary files differ
diff --git a/res/layout/card_border.xml b/res/layout/card_border.xml
index 12be259..dc2d71c 100644
--- a/res/layout/card_border.xml
+++ b/res/layout/card_border.xml
@@ -25,14 +25,24 @@
         android:id="@+id/card_bottom"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_marginStart="@dimen/conversation_view_margin_side"
-        android:layout_marginLeft="@dimen/conversation_view_margin_side"
-        android:layout_marginEnd="@dimen/conversation_view_margin_side"
-        android:layout_marginRight="@dimen/conversation_view_margin_side"
+        android:layout_marginStart="@dimen/conversation_view_margin_side_minus_gradient"
+        android:layout_marginLeft="@dimen/conversation_view_margin_side_minus_gradient"
+        android:layout_marginEnd="@dimen/conversation_view_margin_side_minus_gradient"
+        android:layout_marginRight="@dimen/conversation_view_margin_side_minus_gradient"
         android:layout_gravity="top"
         android:background="@drawable/card_bottom" />
     <Space
         android:id="@+id/border_space"
         android:layout_width="match_parent"
         android:layout_height="@dimen/message_border_height" />
+    <View
+        android:id="@+id/card_top"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/conversation_view_margin_side_minus_gradient"
+        android:layout_marginLeft="@dimen/conversation_view_margin_side_minus_gradient"
+        android:layout_marginEnd="@dimen/conversation_view_margin_side_minus_gradient"
+        android:layout_marginRight="@dimen/conversation_view_margin_side_minus_gradient"
+        android:layout_gravity="bottom"
+        android:background="@drawable/card_top" />
 </com.android.mail.browse.BorderView>
diff --git a/res/layout/conversation_message_header.xml b/res/layout/conversation_message_header.xml
index 8734131..21c111c 100644
--- a/res/layout/conversation_message_header.xml
+++ b/res/layout/conversation_message_header.xml
@@ -20,7 +20,6 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:visibility="visible"
-    android:background="@color/message_header_background_color"
     android:orientation="vertical"
     android:layout_marginStart="@dimen/conversation_view_margin_side"
     android:layout_marginEnd="@dimen/conversation_view_margin_side"
@@ -33,7 +32,7 @@
     <View
         android:id="@+id/snap_header_bottom_border"
         android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:background="@color/conversation_view_border_color"
+        android:layout_height="12dp"
+        android:background="@drawable/snap_header_gradient"
         android:visibility="gone" />
 </com.android.mail.browse.MessageHeaderView>
diff --git a/res/layout/conversation_message_upper_header.xml b/res/layout/conversation_message_upper_header.xml
index 82ede70..3e9d2e8 100644
--- a/res/layout/conversation_message_upper_header.xml
+++ b/res/layout/conversation_message_upper_header.xml
@@ -18,6 +18,7 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:background="@color/message_header_background_color"
     android:duplicateParentState="true" >
 
     <QuickContactBadge
diff --git a/res/values-sw600dp/dimen.xml b/res/values-sw600dp/dimen.xml
index ba74f9e..93184c4 100644
--- a/res/values-sw600dp/dimen.xml
+++ b/res/values-sw600dp/dimen.xml
@@ -24,6 +24,7 @@
     <dimen name="conversation_header_side_padding">40dip</dimen>
     <dimen name="conversation_page_gutter">0dip</dimen>
     <dimen name="message_border_height">16dp</dimen>
+    <dimen name="message_border_height_with_card">23dp</dimen>
     <dimen name="message_header_padding_start">24dp</dimen>
     <dimen name="message_header_inner_side_padding">16dp</dimen>
     <dimen name="message_header_contact_photo_width">64dp</dimen>
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 0c69b73..eef4024 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -42,6 +42,7 @@
     <dimen name="conversation_message_content_margin_side">16dp</dimen>
     <dimen name="conversation_view_margin_side">8dp</dimen>
     <dimen name="conversation_view_margin_side_minus_gradient">7dp</dimen>
+    <dimen name="conversation_view_margin_side_gradient">1dp</dimen>
     <dimen name="conversation_header_font_size">18sp</dimen>
     <dimen name="conversation_header_font_size_condensed">14sp</dimen>
     <dimen name="conversation_header_vertical_padding">16dip</dimen>
@@ -57,6 +58,8 @@
     <dimen name="message_details_header_padding_end">10dip</dimen>
     <dimen name="message_details_header_vertical_padding">7dp</dimen>
     <dimen name="message_border_height">8dp</dimen>
+    <dimen name="message_border_height_collapsed">1dp</dimen>
+    <dimen name="message_border_height_with_card">15dp</dimen>
     <dimen name="message_header_presence_top_margin">-4dp</dimen>
     <dimen name="message_header_action_button_width">48dp</dimen>
     <dimen name="message_header_action_button_height">48dp</dimen>
diff --git a/src/com/android/mail/browse/BorderView.java b/src/com/android/mail/browse/BorderView.java
index 79322b4..1fab0a8 100644
--- a/src/com/android/mail/browse/BorderView.java
+++ b/src/com/android/mail/browse/BorderView.java
@@ -17,18 +17,30 @@
 package com.android.mail.browse;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
+import com.android.mail.R;
 import com.android.mail.browse.ConversationViewAdapter.BorderItem;
 
-import com.android.mail.R;
-
+/**
+ * View displaying the border between messages.
+ * Contains two nine-patches and a {@link android.widget.Space}.
+ * The nine patches are the bottom of the preceding message
+ * and the top of the following message.
+ */
 public class BorderView extends LinearLayout {
 
+    private static int sMessageBorderSpaceHeight = -1;
+    private static int sMessageBorderHeightCollapsed = -1;
+    private static int sMessageBorderHeightWithCard = -1;
+
     private View mCardBottom;
     private View mBorderSpace;
+    private View mCardTop;
 
     public BorderView(Context context) {
         this(context, null);
@@ -40,6 +52,22 @@
 
     public BorderView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+
+        // In order to update the height appropriately based on
+        // whether the border is expanded or collapsed,
+        // we want to stash the height values for for the
+        // space in both its expanded and collapsed values.
+        // Additionally, we stash the total height of the view
+        // when both nine patches are visible.
+        if (sMessageBorderSpaceHeight == -1) {
+            final Resources res = context.getResources();
+            sMessageBorderSpaceHeight =
+                    res.getDimensionPixelSize(R.dimen.message_border_height);
+            sMessageBorderHeightCollapsed = res.getDimensionPixelSize(
+                    R.dimen.message_border_height_collapsed);
+            sMessageBorderHeightWithCard = res.getDimensionPixelSize(
+                    R.dimen.message_border_height_with_card);
+        }
     }
 
     @Override
@@ -48,9 +76,36 @@
 
         mCardBottom = findViewById(R.id.card_bottom);
         mBorderSpace = findViewById(R.id.border_space);
+        mCardTop = findViewById(R.id.card_top);
     }
 
     public void bind(BorderItem borderItem, boolean measureOnly) {
-        mCardBottom.setVisibility(borderItem.isFirstBorder() ? GONE : VISIBLE);
+        final boolean isExpanded = borderItem.isExpanded();
+
+        // Selectively show/hide the card nine-patches if the border is expanded or collapsed.
+        // Additionally this will occur if this is the first or last border.
+        mCardBottom.setVisibility(!isExpanded || borderItem.isFirstBorder() ? GONE : VISIBLE);
+        mCardTop.setVisibility(!isExpanded || borderItem.isLastBorder() ? GONE : VISIBLE);
+
+        // Adjust space height based on expanded state.
+        final ViewGroup.LayoutParams params = mBorderSpace.getLayoutParams();
+        params.height = isExpanded ? sMessageBorderSpaceHeight : sMessageBorderHeightCollapsed;
+        mBorderSpace.setLayoutParams(params);
+    }
+
+    /**
+     * Returns the full expanded height value of the border view.
+     * This height should never change.
+     */
+    public static int getExpandedHeight() {
+        return sMessageBorderHeightWithCard;
+    }
+
+    /**
+     * Returns the collapsed height value of the border view.
+     * This height should never change.
+     */
+    public static int getCollapsedHeight() {
+        return sMessageBorderHeightCollapsed;
     }
 }
diff --git a/src/com/android/mail/browse/ConversationContainer.java b/src/com/android/mail/browse/ConversationContainer.java
index b6e2054..192278b 100644
--- a/src/com/android/mail/browse/ConversationContainer.java
+++ b/src/com/android/mail/browse/ConversationContainer.java
@@ -739,6 +739,7 @@
             } else {
                 traceLayout("move overlay %d", adapterIndex);
                 if (!item.isMeasurementValid()) {
+                    item.rebindView(overlayView);
                     measureOverlayView(overlayView);
                     item.markMeasurementValid();
                     traceLayout("and (re)measure overlay %d, old/new heights=%d/%d", adapterIndex,
diff --git a/src/com/android/mail/browse/ConversationOverlayItem.java b/src/com/android/mail/browse/ConversationOverlayItem.java
index cae878f..f9253f3 100644
--- a/src/com/android/mail/browse/ConversationOverlayItem.java
+++ b/src/com/android/mail/browse/ConversationOverlayItem.java
@@ -35,6 +35,8 @@
 
     public static final String LOG_TAG = ConversationViewFragment.LAYOUT_TAG;
 
+    private int mPosition;
+
     /**
      * @see Adapter#getItemViewType(int)
      */
@@ -60,6 +62,13 @@
      */
     public abstract boolean isContiguous();
 
+    /**
+     * Returns true if this overlay view is in its expanded state.
+     */
+    public boolean isExpanded() {
+        return true;
+    }
+
     public int getGravity() {
         return Gravity.BOTTOM;
     }
@@ -137,4 +146,29 @@
     public void onModelUpdated(View v) {
     }
 
+    public void setPosition(int position) {
+        mPosition = position;
+    }
+
+    public int getPosition() {
+        return mPosition;
+    }
+
+    /**
+     * This is a hack. Now that one view can update the
+     * state of another view, we need a mechanism when the
+     * view's associated item changes to update the state of the
+     * view. Typically, classes that override this class should not
+     * override this method. This method is used by
+     * {@link com.android.mail.browse.ConversationViewAdapter.BorderItem}
+     * to update the height of the border based on whether the neighboring messages
+     * are collapsed or expanded. The only other way would be to
+     * {@link com.android.mail.browse.ConversationViewAdapter#notifyDataSetChanged()}
+     * but that makes the entire screen flicker since the entire adapter performs
+     * a layout of the every item.
+     * @param view the view to be re-bound
+     */
+    public void rebindView(View view) {
+        // DO NOTHING
+    }
 }
diff --git a/src/com/android/mail/browse/ConversationViewAdapter.java b/src/com/android/mail/browse/ConversationViewAdapter.java
index 620a5fc..64fc2f4 100644
--- a/src/com/android/mail/browse/ConversationViewAdapter.java
+++ b/src/com/android/mail/browse/ConversationViewAdapter.java
@@ -187,6 +187,7 @@
             return !isExpanded();
         }
 
+        @Override
         public boolean isExpanded() {
             return mExpanded;
         }
@@ -242,6 +243,10 @@
                 mTimestampLong = mDateBuilder.formatLongDateTime(mTimestampMs);
             }
         }
+
+        public ConversationViewAdapter getAdapter() {
+            return mAdapter;
+        }
     }
 
     public class MessageFooterItem extends ConversationOverlayItem {
@@ -249,10 +254,10 @@
          * A footer can only exist if there is a matching header. Requiring a header allows a
          * footer to stay in sync with the expanded state of the header.
          */
-        private final MessageHeaderItem headerItem;
+        private final MessageHeaderItem mHeaderitem;
 
         private MessageFooterItem(MessageHeaderItem item) {
-            headerItem = item;
+            mHeaderitem = item;
         }
 
         @Override
@@ -271,7 +276,7 @@
         @Override
         public void bindView(View v, boolean measureOnly) {
             final MessageFooterView attachmentsView = (MessageFooterView) v;
-            attachmentsView.bind(headerItem, mAccountController.getAccount().uri, measureOnly);
+            attachmentsView.bind(mHeaderitem, mAccountController.getAccount().uri, measureOnly);
         }
 
         @Override
@@ -280,6 +285,11 @@
         }
 
         @Override
+        public boolean isExpanded() {
+            return mHeaderitem.isExpanded();
+        }
+
+        @Override
         public int getGravity() {
             // attachments are top-aligned within their spacer area
             // Attachments should stay near the body they belong to, even when zoomed far in.
@@ -290,7 +300,7 @@
         public int getHeight() {
             // a footer may change height while its view does not exist because it is offscreen
             // (but the header is onscreen and thus collapsible)
-            if (!headerItem.isExpanded()) {
+            if (!mHeaderitem.isExpanded()) {
                 return 0;
             }
             return super.getHeight();
@@ -331,6 +341,11 @@
             return true;
         }
 
+        @Override
+        public boolean isExpanded() {
+            return false;
+        }
+
         public int getStart() {
             return mStart;
         }
@@ -348,11 +363,16 @@
 
     public class BorderItem extends ConversationOverlayItem {
         private final boolean mContiguous;
+        private boolean mExpanded;
         private final boolean mFirstBorder;
+        private boolean mLastBorder;
 
-        public BorderItem(boolean contiguous, boolean firstBorder) {
+        public BorderItem(boolean contiguous, boolean isExpanded,
+                boolean firstBorder, boolean lastBorder) {
             mContiguous = contiguous;
+            mExpanded = isExpanded;
             mFirstBorder = firstBorder;
+            mLastBorder = lastBorder;
         }
 
         @Override
@@ -377,13 +397,39 @@
         }
 
         @Override
+        public boolean isExpanded() {
+            return mExpanded;
+        }
+
+        public void setExpanded(boolean isExpanded) {
+            mExpanded = isExpanded;
+        }
+
+        @Override
         public boolean canPushSnapHeader() {
-            return true;
+            return false;
         }
 
         public boolean isFirstBorder() {
             return mFirstBorder;
         }
+
+        public boolean isLastBorder() {
+            return mLastBorder;
+        }
+
+        public void setIsLastBorder(boolean isLastBorder) {
+            mLastBorder = isLastBorder;
+        }
+
+        public ConversationViewAdapter getAdapter() {
+            return ConversationViewAdapter.this;
+        }
+
+        @Override
+        public void rebindView(View view) {
+            bindView(view, false);
+        }
     }
 
     public ConversationViewAdapter(ControllableActivity controllableActivity,
@@ -460,6 +506,7 @@
 
     public int addItem(ConversationOverlayItem item) {
         final int pos = mItems.size();
+        item.setPosition(pos);
         mItems.add(item);
         return pos;
     }
@@ -495,12 +542,14 @@
         return addItem(new SuperCollapsedBlockItem(start, end));
     }
 
-    public int addBorder(boolean contiguous, boolean firstBorder) {
-        return addItem(new BorderItem(contiguous, firstBorder));
+    public int addBorder(
+            boolean contiguous, boolean expanded, boolean firstBorder, boolean lastBorder) {
+        return addItem(new BorderItem(contiguous, expanded, firstBorder, lastBorder));
     }
 
-    public BorderItem newBorderItem(boolean contiguous, boolean firstBorder) {
-        return new BorderItem(contiguous, firstBorder);
+    public BorderItem newBorderItem(boolean contiguous, boolean expanded) {
+        return new BorderItem(
+                contiguous, expanded, false /* firstBorder */, false /* lastBorder */);
     }
 
     public void replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove,
@@ -512,6 +561,11 @@
 
         mItems.remove(pos);
         mItems.addAll(pos, replacements);
+
+        // update position for all items
+        for (int i = 0, size = mItems.size(); i < size; i++) {
+            mItems.get(i).setPosition(i);
+        }
     }
 
     public void updateItemsForMessage(ConversationMessage message,
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index feafb9a..29df7ae 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -30,7 +30,6 @@
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.TextUtils;
-import android.text.style.ForegroundColorSpan;
 import android.text.style.StyleSpan;
 import android.text.style.URLSpan;
 import android.util.AttributeSet;
@@ -51,6 +50,7 @@
 import com.android.mail.ContactInfo;
 import com.android.mail.ContactInfoSource;
 import com.android.mail.R;
+import com.android.mail.browse.ConversationViewAdapter.BorderItem;
 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
 import com.android.mail.compose.ComposeActivity;
 import com.android.mail.perf.Timer;
@@ -221,7 +221,8 @@
     public interface MessageHeaderViewCallbacks {
         void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
 
-        void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight);
+        void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight,
+                int topBorderHeight, int bottomBorderHeight);
 
         void setMessageDetailsExpanded(MessageHeaderItem messageHeaderItem, boolean expanded,
                 int previousMessageHeaderItemHeight);
@@ -967,16 +968,83 @@
 
         updateChildVisibility();
 
+        final BorderHeights borderHeights = updateBorderExpandedState();
+
         // Force-measure the new header height so we can set the spacer size and
         // reveal the message div in one pass. Force-measuring makes it unnecessary to set
         // mSizeChanged.
         int h = measureHeight();
         mMessageHeaderItem.setHeight(h);
         if (mCallbacks != null) {
-            mCallbacks.setMessageExpanded(mMessageHeaderItem, h);
+            mCallbacks.setMessageExpanded(mMessageHeaderItem, h,
+                    borderHeights.topHeight, borderHeights.bottomHeight);
         }
     }
 
+    /**
+     * Checks the neighboring messages to this message and
+     * updates the {@link BorderItem}s of the borders of this message
+     * in case they should be collapsed or expanded.
+     * @return a {@link BorderHeights} object containing
+     * the new heights of the top and bottom borders.
+     */
+    private BorderHeights updateBorderExpandedState() {
+        final int position = mMessageHeaderItem.getPosition();
+        final boolean isExpanded = mMessageHeaderItem.isExpanded();
+        final int abovePosition = position - 2; // position of MessageFooterItem above header
+        final int belowPosition = position + 3; // position of next MessageHeaderItem
+        final ConversationViewAdapter adapter = mMessageHeaderItem.getAdapter();
+        final int size = adapter.getCount();
+        final BorderHeights borderHeights = new BorderHeights();
+
+        // if an above message exists, update the border above this message
+        if (isValidPosition(abovePosition, size)) {
+            final ConversationOverlayItem item = adapter.getItem(abovePosition);
+            final int type = item.getType();
+            if (type == ConversationViewAdapter.VIEW_TYPE_MESSAGE_FOOTER ||
+                    type == ConversationViewAdapter.VIEW_TYPE_SUPER_COLLAPSED_BLOCK) {
+                final BorderItem borderItem = (BorderItem) adapter.getItem(abovePosition + 1);
+                final boolean borderIsExpanded = isExpanded || item.isExpanded();
+                borderItem.setExpanded(borderIsExpanded);
+                borderHeights.topHeight = borderIsExpanded ?
+                        BorderView.getExpandedHeight() : BorderView.getCollapsedHeight();
+                borderItem.setHeight(borderHeights.topHeight);
+            }
+        }
+
+
+        // if a below message exists, update the border below this message
+        if (isValidPosition(belowPosition, size)) {
+            final ConversationOverlayItem item = adapter.getItem(belowPosition);
+            if (item.getType() == ConversationViewAdapter.VIEW_TYPE_MESSAGE_HEADER) {
+                final BorderItem borderItem = (BorderItem) adapter.getItem(belowPosition - 1);
+                final boolean borderIsExpanded = isExpanded || item.isExpanded();
+                borderItem.setExpanded(borderIsExpanded);
+                borderHeights.bottomHeight = borderIsExpanded ?
+                        BorderView.getExpandedHeight() : BorderView.getCollapsedHeight();
+                borderItem.setHeight(borderHeights.bottomHeight);
+            }
+        }
+
+        return borderHeights;
+    }
+
+    /**
+     * A plain-old-data class used to return the new heights of the top and bottom borders
+     * in {@link #updateBorderExpandedState()}.
+     * If {@link #topHeight} or {@link #bottomHeight} are -1 after returning,
+     * do not update the heights of the spacer for their respective borders
+     * as their state has not changed.
+     */
+    private class BorderHeights {
+        public int topHeight = -1;
+        public int bottomHeight = -1;
+    }
+
+    private boolean isValidPosition(int position, int size) {
+        return position >= 0 && position < size;
+    }
+
     private void toggleMessageDetails(View visibleDetailsView) {
         int heightBefore = measureHeight();
         final boolean detailsExpanded = (visibleDetailsView == mCollapsedDetailsView);
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index e0a45d6..b298e69 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -645,6 +645,9 @@
         ConversationMessage prevCollapsedMsg = null;
         boolean prevSafeForImages = false;
 
+        // Store the previous expanded state so that the border between
+        // the previous and current message can be properly initialized.
+        int previousExpandedState = ExpansionState.NONE;
         while (messageCursor.moveToPosition(++pos)) {
             final ConversationMessage msg = messageCursor.getMessage();
 
@@ -693,15 +696,18 @@
                 // This line puts the from address in the address cache so that
                 // we get the sender image for it if it's in a super-collapsed block.
                 getAddress(msg.getFrom());
+                previousExpandedState = expandedState;
                 continue;
             }
 
             // resolve any deferred decisions on previous collapsed items
             if (collapsedStart >= 0) {
                 if (pos - collapsedStart == 1) {
-                    // special-case for a single collapsed message: no need to super-collapse it
-                    renderMessage(prevCollapsedMsg, false /* expanded */,
-                            prevSafeForImages, true /* firstBorder */);
+                    // Special-case for a single collapsed message: no need to super-collapse it.
+                    // Since it is super-collapsed, there is no previous message to be
+                    // collapsed and the border above it is the first border.
+                    renderMessage(prevCollapsedMsg, false /* previousCollapsed */,
+                            false /* expanded */, prevSafeForImages, true /* firstBorder */);
                 } else {
                     renderSuperCollapsedBlock(collapsedStart, pos - 1);
                 }
@@ -709,15 +715,19 @@
                 collapsedStart = -1;
             }
 
-            renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages,
+            renderMessage(msg, ExpansionState.isCollapsed(previousExpandedState),
+                    ExpansionState.isExpanded(expandedState), safeForImages,
                     pos == 0 /* firstBorder */);
+
+            previousExpandedState = expandedState;
         }
 
         mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
 
         final boolean applyTransforms = shouldApplyTransforms();
 
-        renderBorder(true /* contiguous */, false /* firstBorder */);
+        renderBorder(true /* contiguous */, true /* expanded */,
+                false /* firstBorder */, true /* lastBorder */);
 
         // If the conversation has specified a base uri, use it here, otherwise use mBaseUri
         return mTemplates.endConversation(mBaseUri, mConversation.getBaseUri(mBaseUri), 320,
@@ -726,27 +736,33 @@
     }
 
     private void renderSuperCollapsedBlock(int start, int end) {
-        renderBorder(true /* contiguous */, true /* firstBorder */);
+        renderBorder(true /* contiguous */, true /* expanded */,
+                true /* firstBorder */, false /* lastBorder */);
         final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
         final int blockPx = measureOverlayHeight(blockPos);
         mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
     }
 
-    protected void renderBorder(boolean contiguous, boolean firstBorder) {
-        final int blockPos = mAdapter.addBorder(contiguous, firstBorder);
+    protected void renderBorder(
+            boolean contiguous, boolean expanded, boolean firstBorder, boolean lastBorder) {
+        final int blockPos = mAdapter.addBorder(contiguous, expanded, firstBorder, lastBorder);
         final int blockPx = measureOverlayHeight(blockPos);
         mTemplates.appendBorder(mWebView.screenPxToWebPx(blockPx));
     }
 
-    private void renderMessage(ConversationMessage msg, boolean expanded,
-            boolean safeForImages, boolean firstBorder) {
-        renderMessage(msg, expanded, safeForImages, true /* renderBorder */, firstBorder);
+    private void renderMessage(ConversationMessage msg, boolean previousCollapsed,
+            boolean expanded, boolean safeForImages, boolean firstBorder) {
+        renderMessage(msg, previousCollapsed, expanded, safeForImages,
+                true /* renderBorder */, firstBorder);
     }
 
-    private void renderMessage(ConversationMessage msg, boolean expanded,
-            boolean safeForImages, boolean renderBorder, boolean firstBorder) {
+    private void renderMessage(ConversationMessage msg, boolean previousCollapsed,
+            boolean expanded, boolean safeForImages, boolean renderBorder, boolean firstBorder) {
         if (renderBorder) {
-            renderBorder(true /* contiguous */, firstBorder);
+            // The border should be collapsed only if both the current
+            // and previous messages are collapsed.
+            renderBorder(true /* contiguous */, !previousCollapsed || expanded,
+                    firstBorder, false /* lastBorder */);
         }
 
         final int headerPos = mAdapter.addMessageHeader(msg, expanded,
@@ -785,8 +801,10 @@
                 borderPx = 0;
                 first = false;
             } else {
-                final BorderItem border =
-                        mAdapter.newBorderItem(true /* contiguous */, false /* firstBorder */);
+                // When replacing the super-collapsed block,
+                // the border is always collapsed between messages.
+                final BorderItem border = mAdapter.newBorderItem(
+                        true /* contiguous */, false /* expanded */);
                 borderPx = measureOverlayHeight(border);
                 replacements.add(border);
                 mTemplates.appendBorder(mWebView.screenPxToWebPx(borderPx));
@@ -881,15 +899,19 @@
     }
 
     @Override
-    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
+    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx,
+            int topBorderHeight, int bottomBorderHeight) {
         mConversationContainer.invalidateSpacerGeometry();
 
         // show/hide the HTML message body and update the spacer height
         final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
+        final int topHeight = mWebView.screenPxToWebPx(topBorderHeight);
+        final int bottomHeight = mWebView.screenPxToWebPx(bottomBorderHeight);
         LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
                 item.isExpanded(), h, newSpacerHeightPx);
-        mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s);",
-                mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(), h));
+        mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s, %s, %s);",
+                mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(),
+                h, topHeight, bottomHeight));
 
         mViewState.setExpansionState(item.getMessage(),
                 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
@@ -1411,12 +1433,21 @@
     }
 
     private void processNewOutgoingMessage(ConversationMessage msg) {
+        // make the last border no longer be the border
+        ((BorderItem) mAdapter.getItem(mAdapter.getCount() - 1)).setIsLastBorder(false);
+
         mTemplates.reset();
         // this method will add some items to mAdapter, but we deliberately want to avoid notifying
         // adapter listeners (i.e. ConversationContainer) until onWebContentGeometryChange is next
         // called, to prevent N+1 headers rendering with N message bodies.
-        renderMessage(msg, true /* expanded */, msg.alwaysShowImages, false /* renderBorder */);
-        renderBorder(true /* contiguous */, false /* firstBorder */);
+
+        // We can just call previousCollapsed false here since the border
+        // above the message we're about to render should always show
+        // (which it also will since the message being render is expanded).
+        renderMessage(msg, false /* previousCollapsed */, true /* expanded */,
+                msg.alwaysShowImages, false /* renderBorder */, false /* firstBorder */);
+        renderBorder(true /* contiguous */, true /* expanded */,
+                false /* firstBorder */, true /* lastBorder */);
         mTempBodiesHtml = mTemplates.emit();
 
         mViewState.setExpansionState(msg, ExpansionState.EXPANDED);
diff --git a/src/com/android/mail/ui/ConversationViewState.java b/src/com/android/mail/ui/ConversationViewState.java
index 2d733fa..b93c990 100644
--- a/src/com/android/mail/ui/ConversationViewState.java
+++ b/src/com/android/mail/ui/ConversationViewState.java
@@ -43,6 +43,7 @@
     private byte[] mConversationInfo;
 
     public static final class ExpansionState {
+        public static int NONE = 0;
         public static int EXPANDED = 1;
         public static int COLLAPSED = 2;
         public static int SUPER_COLLAPSED = 3;
@@ -55,6 +56,14 @@
         public static boolean isSuperCollapsed(int state) {
             return state == SUPER_COLLAPSED;
         }
+
+        /**
+         * Returns true if the {@link ExpansionState} is
+         * {@link #COLLAPSED} or {@link #SUPER_COLLAPSED}.
+         */
+        public static boolean isCollapsed(int state) {
+            return state > EXPANDED;
+        }
     }
 
     public ConversationViewState() {}
diff --git a/src/com/android/mail/ui/SecureConversationViewController.java b/src/com/android/mail/ui/SecureConversationViewController.java
index 77f618e..55a466b 100644
--- a/src/com/android/mail/ui/SecureConversationViewController.java
+++ b/src/com/android/mail/ui/SecureConversationViewController.java
@@ -178,7 +178,8 @@
     }
 
     @Override
-    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight) {
+    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight,
+            int topBorderHeight, int bottomBorderHeight) {
         // Do nothing.
     }