Redesigned the messaging style

The layout now looks much more recognizable
as a messaging template and enables us to
prepare for more useful functionality.

Test: Send messages and observe display
Bug: 63708826
Change-Id: I896b3692a1e84976e8fd37cf37611ddb1d358fb9
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 8226e0f..f0e09b7 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -22,6 +22,7 @@
 import android.annotation.DrawableRes;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
@@ -3900,7 +3901,7 @@
             final Bundle ex = mN.extras;
             updateBackgroundColor(contentView);
             bindNotificationHeader(contentView, p.ambient);
-            bindLargeIcon(contentView);
+            bindLargeIcon(contentView, p.hideLargeIcon, p.alwaysShowReply);
             boolean showProgress = handleProgressBar(p.hasProgress, contentView, ex);
             if (p.title != null) {
                 contentView.setViewVisibility(R.id.title, View.VISIBLE);
@@ -4110,11 +4111,13 @@
             }
         }
 
-        private void bindLargeIcon(RemoteViews contentView) {
+        private void bindLargeIcon(RemoteViews contentView, boolean hideLargeIcon,
+                boolean alwaysShowReply) {
             if (mN.mLargeIcon == null && mN.largeIcon != null) {
                 mN.mLargeIcon = Icon.createWithBitmap(mN.largeIcon);
             }
-            if (mN.mLargeIcon != null) {
+            boolean showLargeIcon = mN.mLargeIcon != null && !hideLargeIcon;
+            if (showLargeIcon) {
                 contentView.setViewVisibility(R.id.right_icon, View.VISIBLE);
                 contentView.setImageViewIcon(R.id.right_icon, mN.mLargeIcon);
                 processLargeLegacyIcon(mN.mLargeIcon, contentView);
@@ -4122,32 +4125,45 @@
                 contentView.setViewLayoutMarginEndDimen(R.id.line1, endMargin);
                 contentView.setViewLayoutMarginEndDimen(R.id.text, endMargin);
                 contentView.setViewLayoutMarginEndDimen(R.id.progress, endMargin);
-                // Bind the reply action
-                Action action = findReplyAction();
-                contentView.setViewVisibility(R.id.reply_icon_action, action != null
-                        ? View.VISIBLE
-                        : View.GONE);
+            }
+            // Bind the reply action
+            Action action = findReplyAction();
 
-                if (action != null) {
-                    int contrastColor = resolveContrastColor();
+            boolean actionVisible = action != null && (showLargeIcon || alwaysShowReply);
+            int replyId = showLargeIcon ? R.id.reply_icon_action : R.id.right_icon;
+            if (actionVisible) {
+                // We're only showing the icon as big if we're hiding the large icon
+                int contrastColor = resolveContrastColor();
+                int iconColor;
+                if (showLargeIcon) {
                     contentView.setDrawableTint(R.id.reply_icon_action,
                             true /* targetBackground */,
                             contrastColor, PorterDuff.Mode.SRC_ATOP);
-                    int iconColor = NotificationColorUtil.isColorLight(contrastColor)
-                            ? Color.BLACK : Color.WHITE;
-                    contentView.setDrawableTint(R.id.reply_icon_action,
-                            false /* targetBackground */,
-                            iconColor, PorterDuff.Mode.SRC_ATOP);
                     contentView.setOnClickPendingIntent(R.id.right_icon,
                             action.actionIntent);
-                    contentView.setOnClickPendingIntent(R.id.reply_icon_action,
-                            action.actionIntent);
                     contentView.setRemoteInputs(R.id.right_icon, action.mRemoteInputs);
-                    contentView.setRemoteInputs(R.id.reply_icon_action, action.mRemoteInputs);
-
+                    iconColor = NotificationColorUtil.isColorLight(contrastColor)
+                            ? Color.BLACK : Color.WHITE;
+                } else {
+                    contentView.setImageViewResource(R.id.right_icon,
+                            R.drawable.ic_reply_notification_large);
+                    contentView.setViewVisibility(R.id.right_icon, View.VISIBLE);
+                    iconColor = contrastColor;
                 }
+                contentView.setDrawableTint(replyId,
+                        false /* targetBackground */,
+                        iconColor,
+                        PorterDuff.Mode.SRC_ATOP);
+                contentView.setOnClickPendingIntent(replyId,
+                        action.actionIntent);
+                contentView.setRemoteInputs(replyId, action.mRemoteInputs);
+            } else {
+                contentView.setRemoteInputs(R.id.right_icon, null);
             }
-            contentView.setViewVisibility(R.id.right_icon_container, mN.mLargeIcon != null
+            contentView.setViewVisibility(R.id.reply_icon_action, actionVisible && showLargeIcon
+                    ? View.VISIBLE
+                    : View.GONE);
+            contentView.setViewVisibility(R.id.right_icon_container, actionVisible || showLargeIcon
                     ? View.VISIBLE
                     : View.GONE);
         }
@@ -6055,18 +6071,12 @@
         protected void restoreFromExtras(Bundle extras) {
             super.restoreFromExtras(extras);
 
-            mMessages.clear();
-            mHistoricMessages.clear();
             mUserDisplayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
             mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
             Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
-            if (messages != null && messages instanceof Parcelable[]) {
-                mMessages = Message.getMessagesFromBundleArray(messages);
-            }
+            mMessages = Message.getMessagesFromBundleArray(messages);
             Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES);
-            if (histMessages != null && histMessages instanceof Parcelable[]) {
-                mHistoricMessages = Message.getMessagesFromBundleArray(histMessages);
-            }
+            mHistoricMessages = Message.getMessagesFromBundleArray(histMessages);
         }
 
         /**
@@ -6096,16 +6106,25 @@
         }
 
         private Message findLatestIncomingMessage() {
-            for (int i = mMessages.size() - 1; i >= 0; i--) {
-                Message m = mMessages.get(i);
+            return findLatestIncomingMessage(mMessages);
+        }
+
+        /**
+         * @hide
+         */
+        @Nullable
+        public static Message findLatestIncomingMessage(
+                List<Message> messages) {
+            for (int i = messages.size() - 1; i >= 0; i--) {
+                Message m = messages.get(i);
                 // Incoming messages have a non-empty sender.
                 if (!TextUtils.isEmpty(m.mSender)) {
                     return m;
                 }
             }
-            if (!mMessages.isEmpty()) {
+            if (!messages.isEmpty()) {
                 // No incoming messages, fall back to outgoing message
-                return mMessages.get(mMessages.size() - 1);
+                return messages.get(messages.size() - 1);
             }
             return null;
         }
@@ -6115,95 +6134,57 @@
          */
         @Override
         public RemoteViews makeBigContentView() {
-            CharSequence title = !TextUtils.isEmpty(super.mBigContentTitle)
+            CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
                     ? super.mBigContentTitle
                     : mConversationTitle;
-            boolean hasTitle = !TextUtils.isEmpty(title);
-
-            if (mMessages.size() == 1) {
-                // Special case for a single message: Use the big text style
-                // so the collapsed and expanded versions match nicely.
-                CharSequence bigTitle;
-                CharSequence text;
-                if (hasTitle) {
-                    bigTitle = title;
-                    text = makeMessageLine(mMessages.get(0), mBuilder);
-                } else {
-                    bigTitle = mMessages.get(0).mSender;
-                    text = mMessages.get(0).mText;
-                }
-                RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
-                        mBuilder.getBigTextLayoutResource(),
-                        mBuilder.mParams.reset().hasProgress(false).title(bigTitle).text(null));
-                BigTextStyle.applyBigTextContentView(mBuilder, contentView, text);
-                return contentView;
+            boolean isOneToOne = TextUtils.isEmpty(conversationTitle);
+            if (isOneToOne) {
+                // Let's add the conversationTitle in case we didn't have one before and all
+                // messages are from the same sender
+                conversationTitle = createConversationTitleFromMessages();
             }
-
+            boolean hasTitle = !TextUtils.isEmpty(conversationTitle);
             RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
                     mBuilder.getMessagingLayoutResource(),
-                    mBuilder.mParams.reset().hasProgress(false).title(title).text(null));
-
-            int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3,
-                    R.id.inbox_text4, R.id.inbox_text5, R.id.inbox_text6};
-
-            // Make sure all rows are gone in case we reuse a view.
-            for (int rowId : rowIds) {
-                contentView.setViewVisibility(rowId, View.GONE);
-            }
-
-            int i=0;
+                    mBuilder.mParams.reset().hasProgress(false).title(conversationTitle).text(null)
+                            .hideLargeIcon(isOneToOne).alwaysShowReply(true));
             contentView.setViewLayoutMarginBottomDimen(R.id.line1,
                     hasTitle ? R.dimen.notification_messaging_spacing : 0);
             contentView.setInt(R.id.notification_messaging, "setNumIndentLines",
                     !mBuilder.mN.hasLargeIcon() ? 0 : (hasTitle ? 1 : 2));
-
-            int contractedChildId = View.NO_ID;
-            Message contractedMessage = findLatestIncomingMessage();
-            int firstHistoricMessage = Math.max(0, mHistoricMessages.size()
-                    - (rowIds.length - mMessages.size()));
-            while (firstHistoricMessage + i < mHistoricMessages.size() && i < rowIds.length) {
-                Message m = mHistoricMessages.get(firstHistoricMessage + i);
-                int rowId = rowIds[i];
-
-                contentView.setTextViewText(rowId, makeMessageLine(m, mBuilder));
-
-                if (contractedMessage == m) {
-                    contractedChildId = rowId;
-                }
-
-                i++;
-            }
-
-            int firstMessage = Math.max(0, mMessages.size() - rowIds.length);
-            while (firstMessage + i < mMessages.size() && i < rowIds.length) {
-                Message m = mMessages.get(firstMessage + i);
-                int rowId = rowIds[i];
-
-                contentView.setViewVisibility(rowId, View.VISIBLE);
-                contentView.setTextViewText(rowId, mBuilder.processTextSpans(
-                        makeMessageLine(m, mBuilder)));
-                mBuilder.setTextViewColorSecondary(contentView, rowId);
-
-                if (contractedMessage == m) {
-                    contractedChildId = rowId;
-                }
-
-                i++;
-            }
-            // Clear the remaining views for reapply. Ensures that historic message views can
-            // reliably be identified as being GONE and having non-null text.
-            while (i < rowIds.length) {
-                int rowId = rowIds[i];
-                contentView.setTextViewText(rowId, null);
-                i++;
-            }
-
-            // Record this here to allow transformation between the contracted and expanded views.
-            contentView.setInt(R.id.notification_messaging, "setContractedChildId",
-                    contractedChildId);
+            addExtras(mBuilder.mN.extras);
+            contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor",
+                    mBuilder.resolveContrastColor());
+            contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon",
+                    mBuilder.mN.mLargeIcon);
+            contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsOneToOne",
+                    isOneToOne);
+            contentView.setBundle(R.id.status_bar_latest_event_content, "setData",
+                    mBuilder.mN.extras);
             return contentView;
         }
 
+        private CharSequence createConversationTitleFromMessages() {
+            ArraySet<CharSequence> names = new ArraySet<>();
+            for (int i = 0; i < mMessages.size(); i++) {
+                Message m = mMessages.get(i);
+                CharSequence sender = m.getSender();
+                if (sender != null) {
+                    names.add(sender);
+                }
+            }
+            SpannableStringBuilder title = new SpannableStringBuilder();
+            int size = names.size();
+            for (int i = 0; i < size; i++) {
+                CharSequence name = names.valueAt(i);
+                if (!TextUtils.isEmpty(title)) {
+                    title.append(", ");
+                }
+                title.append(BidiFormatter.getInstance().unicodeWrap(name));
+            }
+            return title;
+        }
+
         private CharSequence makeMessageLine(Message m, Builder builder) {
             BidiFormatter bidi = BidiFormatter.getInstance();
             SpannableStringBuilder sb = new SpannableStringBuilder();
@@ -6394,7 +6375,15 @@
                 return bundles;
             }
 
-            static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) {
+            /**
+             * @return A list of messages read from the bundles.
+             *
+             * @hide
+             */
+            public static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) {
+                if (bundles == null) {
+                    return new ArrayList<>();
+                }
                 List<Message> messages = new ArrayList<>(bundles.length);
                 for (int i = 0; i < bundles.length; i++) {
                     if (bundles[i] instanceof Bundle) {
@@ -8487,6 +8476,8 @@
         boolean ambient = false;
         CharSequence title;
         CharSequence text;
+        boolean hideLargeIcon;
+        public boolean alwaysShowReply;
 
         final StandardTemplateParams reset() {
             hasProgress = true;
@@ -8511,6 +8502,16 @@
             return this;
         }
 
+        final StandardTemplateParams alwaysShowReply(boolean alwaysShowReply) {
+            this.alwaysShowReply = alwaysShowReply;
+            return this;
+        }
+
+        final StandardTemplateParams hideLargeIcon(boolean hideLargeIcon) {
+            this.hideLargeIcon = hideLargeIcon;
+            return this;
+        }
+
         final StandardTemplateParams ambient(boolean ambient) {
             Preconditions.checkState(title == null && text == null, "must set ambient before text");
             this.ambient = ambient;
@@ -8527,7 +8528,6 @@
                 text = extras.getCharSequence(EXTRA_TEXT);
             }
             this.text = b.processLegacyText(text, ambient);
-
             return this;
         }
     }
diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java
index 7870333..09f7282 100644
--- a/core/java/com/android/internal/widget/ImageFloatingTextView.java
+++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java
@@ -176,8 +176,4 @@
         }
         return false;
     }
-
-    public int getLayoutHeight() {
-        return getLayout().getHeight();
-    }
 }
diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java
new file mode 100644
index 0000000..ae08f87
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingGroup.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.util.Pools;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+import com.android.internal.util.NotificationColorUtil;
+
+import java.util.List;
+
+/**
+ * A message of a {@link MessagingLayout}.
+ */
+@RemoteViews.RemoteView
+public class MessagingGroup extends LinearLayout implements MessagingLinearLayout.MessagingChild {
+
+    private static Pools.SimplePool<MessagingGroup> sInstancePool
+            = new Pools.SynchronizedPool<>(10);
+    private MessagingLinearLayout mMessageContainer;
+    private ImageFloatingTextView mSenderName;
+    private ImageView mAvatarView;
+    private String mAvatarSymbol = "";
+    private int mLayoutColor;
+    private CharSequence mAvatarName = "";
+    private Icon mAvatarIcon;
+    private ColorFilter mMessageBackgroundFilter;
+    private int mTextColor;
+
+    public MessagingGroup(@NonNull Context context) {
+        super(context);
+    }
+
+    public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mMessageContainer = findViewById(R.id.group_message_container);
+        mSenderName = findViewById(R.id.message_name);
+        mAvatarView = findViewById(R.id.message_icon);
+    }
+
+    public void setSender(CharSequence sender) {
+        if (sender == null) {
+            mAvatarView.setVisibility(GONE);
+            mSenderName.setVisibility(GONE);
+            setGravity(Gravity.END);
+            mMessageBackgroundFilter = new PorterDuffColorFilter(mLayoutColor,
+                    PorterDuff.Mode.SRC_ATOP);
+            mTextColor = NotificationColorUtil.isColorLight(mLayoutColor) ? getNormalTextColor()
+                    : Color.WHITE;
+        } else {
+            mSenderName.setText(sender);
+            mAvatarView.setVisibility(VISIBLE);
+            mSenderName.setVisibility(VISIBLE);
+            setGravity(Gravity.START);
+            mMessageBackgroundFilter = null;
+            mTextColor = getNormalTextColor();
+        }
+    }
+
+    private int getNormalTextColor() {
+        return mContext.getColor(R.color.notification_primary_text_color_light);
+    }
+
+    public void setAvatar(Icon icon) {
+        mAvatarIcon = icon;
+        mAvatarView.setImageIcon(icon);
+        mAvatarSymbol = "";
+        mLayoutColor = 0;
+        mAvatarName = "";
+    }
+
+    static MessagingGroup createGroup(MessagingLinearLayout layout) {;
+        MessagingGroup createdGroup = sInstancePool.acquire();
+        if (createdGroup == null) {
+            createdGroup = (MessagingGroup) LayoutInflater.from(layout.getContext()).inflate(
+                    R.layout.notification_template_messaging_group, layout,
+                    false);
+        }
+        layout.addView(createdGroup);
+        return createdGroup;
+    }
+
+    public void removeMessage(MessagingMessage messagingMessage) {
+        // TODO: add removal animation
+        mMessageContainer.removeView(messagingMessage);
+        if (mMessageContainer.getChildCount() == 0) {
+            ViewParent parent = getParent();
+            if (parent instanceof ViewGroup) {
+                ((ViewGroup) parent).removeView(this);
+            }
+            setAvatar(null);
+            sInstancePool.release(this);
+        }
+    }
+
+    public CharSequence getSenderName() {
+        return mSenderName.getText();
+    }
+
+    public void setSenderVisible(boolean visible) {
+        mSenderName.setVisibility(visible ? VISIBLE : GONE);
+    }
+
+    public static void dropCache() {
+        sInstancePool = new Pools.SynchronizedPool<>(10);
+    }
+
+    @Override
+    public int getMeasuredType() {
+        boolean hasNormal = false;
+        for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) {
+            View child = mMessageContainer.getChildAt(i);
+            if (child instanceof MessagingLinearLayout.MessagingChild) {
+                int type = ((MessagingLinearLayout.MessagingChild) child).getMeasuredType();
+                if (type == MEASURED_TOO_SMALL) {
+                    if (hasNormal) {
+                        return MEASURED_SHORTENED;
+                    } else {
+                        return MEASURED_TOO_SMALL;
+                    }
+                } else if (type == MEASURED_SHORTENED) {
+                    return MEASURED_SHORTENED;
+                } else {
+                    hasNormal = true;
+                }
+            }
+        }
+        return MEASURED_NORMAL;
+    }
+
+    @Override
+    public int getConsumedLines() {
+        int result = 0;
+        for (int i = 0; i < mMessageContainer.getChildCount(); i++) {
+            View child = mMessageContainer.getChildAt(i);
+            if (child instanceof MessagingLinearLayout.MessagingChild) {
+                result += ((MessagingLinearLayout.MessagingChild) child).getConsumedLines();
+            }
+        }
+        return result;
+    }
+
+    public Icon getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol,
+            int layoutColor) {
+        if (mAvatarName.equals(avatarName) && mAvatarSymbol.equals(avatarSymbol)
+                && layoutColor == mLayoutColor) {
+            return mAvatarIcon;
+        }
+        return null;
+    }
+
+    public void setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol,
+            int layoutColor) {
+        if (!mAvatarName.equals(avatarName) || !mAvatarSymbol.equals(avatarSymbol)
+                || layoutColor != mLayoutColor) {
+            setAvatar(cachedIcon);
+            mAvatarSymbol = avatarSymbol;
+            mLayoutColor = layoutColor;
+            mAvatarName = avatarName;
+        }
+    }
+
+    public void setLayoutColor(int layoutColor) {
+        mLayoutColor = layoutColor;
+    }
+
+    public void setMessages(List<MessagingMessage> group) {
+        // Let's now make sure all children are added and in the correct order
+        for (int messageIndex = 0; messageIndex < group.size(); messageIndex++) {
+            MessagingMessage message = group.get(messageIndex);
+            if (message.getGroup() != this) {
+                message.setMessagingGroup(this);
+                ViewParent parent = mMessageContainer.getParent();
+                if (parent instanceof ViewGroup) {
+                    ((ViewGroup) parent).removeView(message);
+                }
+                mMessageContainer.addView(message, messageIndex);
+            }
+            if (messageIndex != mMessageContainer.indexOfChild(message)) {
+                mMessageContainer.removeView(message);
+                mMessageContainer.addView(message, messageIndex);
+            }
+            // Let's make sure the message color is correct
+            Drawable targetDrawable = message.getBackground();
+
+            if (targetDrawable != null) {
+                targetDrawable.mutate().setColorFilter(mMessageBackgroundFilter);
+            }
+            message.setTextColor(mTextColor);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java
new file mode 100644
index 0000000..4fd7e9b
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingLayout.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.util.NotificationColorUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
+ * messages and adapts the layout accordingly.
+ */
+@RemoteViews.RemoteView
+public class MessagingLayout extends FrameLayout {
+
+    private static final float COLOR_SHIFT_AMOUNT = 60;
+    private static final Consumer<MessagingMessage> REMOVE_MESSAGE
+            = MessagingMessage::removeMessage;
+    private List<MessagingMessage> mMessages = new ArrayList<>();
+    private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
+    private MessagingLinearLayout mMessagingLinearLayout;
+    private View mContractedMessage;
+    private boolean mShowHistoricMessages;
+    private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
+    private TextView mTitleView;
+    private int mLayoutColor;
+    private int mAvatarSize;
+    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private Paint mTextPaint = new Paint();
+    private CharSequence mConversationTitle;
+    private Icon mLargeIcon;
+    private boolean mIsOneToOne;
+
+    public MessagingLayout(@NonNull Context context) {
+        super(context);
+    }
+
+    public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mMessagingLinearLayout = findViewById(R.id.notification_messaging);
+        mTitleView = findViewById(R.id.title);
+        mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size);
+        mTextPaint.setTextAlign(Paint.Align.CENTER);
+        mTextPaint.setAntiAlias(true);
+    }
+
+    @RemotableViewMethod
+    public void setLargeIcon(Icon icon) {
+        mLargeIcon = icon;
+    }
+
+    @RemotableViewMethod
+    public void setData(Bundle extras) {
+        Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
+        List<Notification.MessagingStyle.Message> newMessages
+                = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
+        Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
+        List<Notification.MessagingStyle.Message> newHistoricMessages
+                = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
+        mConversationTitle = null;
+        TextView headerText = findViewById(R.id.header_text);
+        if (headerText != null) {
+            mConversationTitle = headerText.getText();
+        }
+        bind(newMessages, newHistoricMessages);
+    }
+
+    private void bind(List<Notification.MessagingStyle.Message> newMessages,
+            List<Notification.MessagingStyle.Message> newHistoricMessages) {
+
+        List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
+                true /* isHistoric */);
+        List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
+        addMessagesToGroups(historicMessages, messages);
+
+        // Let's remove the remaining messages
+        mMessages.forEach(REMOVE_MESSAGE);
+        mHistoricMessages.forEach(REMOVE_MESSAGE);
+
+        mMessages = messages;
+        mHistoricMessages = historicMessages;
+
+        updateContractedMessage();
+        updateHistoricMessageVisibility();
+        updateTitleAndNamesDisplay();
+    }
+
+    private void updateTitleAndNamesDisplay() {
+        ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
+        ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>();
+        for (int i = 0; i < mGroups.size(); i++) {
+            MessagingGroup group = mGroups.get(i);
+            CharSequence senderName = group.getSenderName();
+            if (TextUtils.isEmpty(senderName)) {
+                continue;
+            }
+            boolean visible = !mIsOneToOne;
+            group.setSenderVisible(visible);
+            if ((visible || mLargeIcon == null) && !uniqueNames.containsKey(senderName)) {
+                char c = senderName.charAt(0);
+                if (uniqueCharacters.containsKey(c)) {
+                    // this character was already used, lets make it more unique. We first need to
+                    // resolve the existing character if it exists
+                    CharSequence existingName = uniqueCharacters.get(c);
+                    if (existingName != null) {
+                        uniqueNames.put(existingName, findNameSplit((String) existingName));
+                        uniqueCharacters.put(c, null);
+                    }
+                    uniqueNames.put(senderName, findNameSplit((String) senderName));
+                } else {
+                    uniqueNames.put(senderName, Character.toString(c));
+                    uniqueCharacters.put(c, senderName);
+                }
+            }
+        }
+
+        // Now that we have the correct symbols, let's look what we have cached
+        ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
+        for (int i = 0; i < mGroups.size(); i++) {
+            // Let's now set the avatars
+            MessagingGroup group = mGroups.get(i);
+            CharSequence senderName = group.getSenderName();
+            if (TextUtils.isEmpty(senderName) || (mIsOneToOne && mLargeIcon != null)) {
+                continue;
+            }
+            String symbol = uniqueNames.get(senderName);
+            Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
+                    symbol, mLayoutColor);
+            if (cachedIcon != null) {
+                cachedAvatars.put(senderName, cachedIcon);
+            }
+        }
+
+        for (int i = 0; i < mGroups.size(); i++) {
+            // Let's now set the avatars
+            MessagingGroup group = mGroups.get(i);
+            CharSequence senderName = group.getSenderName();
+            if (TextUtils.isEmpty(senderName)) {
+                continue;
+            }
+            if (mIsOneToOne && mLargeIcon != null) {
+                group.setAvatar(mLargeIcon);
+            } else {
+                Icon cachedIcon = cachedAvatars.get(senderName);
+                if (cachedIcon == null) {
+                    cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
+                            mLayoutColor);
+                    cachedAvatars.put(senderName, cachedIcon);
+                }
+                group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
+                        mLayoutColor);
+            }
+        }
+    }
+
+    public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) {
+        Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        float radius = mAvatarSize / 2.0f;
+        int color = findColor(senderName, layoutColor);
+        mPaint.setColor(color);
+        canvas.drawCircle(radius, radius, radius, mPaint);
+        boolean needDarkText  = ColorUtils.calculateLuminance(color) > 0.5f;
+        mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
+        mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.75f : mAvatarSize * 0.4f);
+        int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)) ;
+        canvas.drawText(symbol, radius, yPos, mTextPaint);
+        return Icon.createWithBitmap(bitmap);
+    }
+
+    private int findColor(CharSequence senderName, int layoutColor) {
+        double luminance = NotificationColorUtil.calculateLuminance(layoutColor);
+        float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
+
+        // we need to offset the range if the luminance is too close to the borders
+        shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
+        shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
+        return NotificationColorUtil.getShiftedColor(layoutColor,
+                (int) (shift * COLOR_SHIFT_AMOUNT));
+    }
+
+    private String findNameSplit(String existingName) {
+        String[] split = existingName.split(" ");
+        if (split.length > 1) {
+            return Character.toString(split[0].charAt(0))
+                    + Character.toString(split[1].charAt(0));
+        }
+        return existingName.substring(0, 1);
+    }
+
+    @RemotableViewMethod
+    public void setLayoutColor(int color) {
+        mLayoutColor = color;
+    }
+
+    @RemotableViewMethod
+    public void setIsOneToOne(boolean oneToOne) {
+        mIsOneToOne = oneToOne;
+    }
+
+    private void addMessagesToGroups(List<MessagingMessage> historicMessages,
+            List<MessagingMessage> messages) {
+        // Let's first find our groups!
+        List<List<MessagingMessage>> groups = new ArrayList<>();
+        List<CharSequence> senders = new ArrayList<>();
+
+        // Lets first find the groups
+        findGroups(historicMessages, messages, groups, senders);
+
+        // Let's now create the views and reorder them accordingly
+        createGroupViews(groups, senders);
+    }
+
+    private void createGroupViews(List<List<MessagingMessage>> groups, List<CharSequence> senders) {
+        mGroups.clear();
+        for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
+            List<MessagingMessage> group = groups.get(groupIndex);
+            MessagingGroup newGroup = null;
+            // we'll just take the first group that exists or create one there is none
+            for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
+                MessagingMessage message = group.get(messageIndex);
+                newGroup = message.getGroup();
+                if (newGroup != null) {
+                    break;
+                }
+            }
+            if (newGroup == null) {
+                newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
+            }
+            newGroup.setLayoutColor(mLayoutColor);
+            newGroup.setSender(senders.get(groupIndex));
+            mGroups.add(newGroup);
+
+            if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
+                mMessagingLinearLayout.removeView(newGroup);
+                mMessagingLinearLayout.addView(newGroup, groupIndex);
+            }
+            newGroup.setMessages(group);
+        }
+    }
+
+    private void findGroups(List<MessagingMessage> historicMessages,
+            List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
+            List<CharSequence> senders) {
+        CharSequence currentSender = null;
+        List<MessagingMessage> currentGroup = null;
+        int histSize = historicMessages.size();
+        for (int i = 0; i < histSize + messages.size(); i++) {
+            MessagingMessage message;
+            if (i < histSize) {
+                message = historicMessages.get(i);
+            } else {
+                message = messages.get(i - histSize);
+            }
+            boolean isNewGroup = currentGroup == null;
+            CharSequence sender = message.getMessage().getSender();
+            isNewGroup |= !TextUtils.equals(sender, currentSender);
+            if (isNewGroup) {
+                currentGroup = new ArrayList<>();
+                groups.add(currentGroup);
+                senders.add(sender);
+                currentSender = sender;
+            }
+            currentGroup.add(message);
+        }
+    }
+
+    private void updateContractedMessage() {
+        for (int i = mMessages.size() - 1; i >= 0; i--) {
+            MessagingMessage m = mMessages.get(i);
+            // Incoming messages have a non-empty sender.
+            if (!TextUtils.isEmpty(m.getMessage().getSender())) {
+                mContractedMessage = m;
+                return;
+            }
+        }
+        if (!mMessages.isEmpty()) {
+            // No incoming messages, fall back to outgoing message
+            mContractedMessage = mMessages.get(mMessages.size() - 1);
+            return;
+        }
+        mContractedMessage = null;
+    }
+
+    /**
+     * Creates new messages, reusing existing ones if they are available.
+     *
+     * @param newMessages the messages to parse.
+     */
+    private List<MessagingMessage> createMessages(
+            List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
+        List<MessagingMessage> result = new ArrayList<>();;
+        for (int i = 0; i < newMessages.size(); i++) {
+            Notification.MessagingStyle.Message m = newMessages.get(i);
+            MessagingMessage message = findAndRemoveMatchingMessage(m);
+            if (message == null) {
+                message = MessagingMessage.createMessage(this, m);
+            }
+            message.setIsHistoric(historic);
+            result.add(message);
+        }
+        return result;
+    }
+
+    private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
+        for (int i = 0; i < mMessages.size(); i++) {
+            MessagingMessage existing = mMessages.get(i);
+            if (existing.sameAs(m)) {
+                mMessages.remove(i);
+                return existing;
+            }
+        }
+        for (int i = 0; i < mHistoricMessages.size(); i++) {
+            MessagingMessage existing = mHistoricMessages.get(i);
+            if (existing.sameAs(m)) {
+                mHistoricMessages.remove(i);
+                return existing;
+            }
+        }
+        return null;
+    }
+
+    public void showHistoricMessages(boolean show) {
+        mShowHistoricMessages = show;
+        updateHistoricMessageVisibility();
+    }
+
+    private void updateHistoricMessageVisibility() {
+        for (int i = 0; i < mHistoricMessages.size(); i++) {
+            MessagingMessage existing = mHistoricMessages.get(i);
+            existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
+        }
+    }
+
+    public View getContractedMessage() {
+        return mContractedMessage;
+    }
+
+    public MessagingLinearLayout getMessagingLinearLayout() {
+        return mMessagingLinearLayout;
+    }
+}
diff --git a/core/java/com/android/internal/widget/MessagingLinearLayout.java b/core/java/com/android/internal/widget/MessagingLinearLayout.java
index 70473a0..e050d45 100644
--- a/core/java/com/android/internal/widget/MessagingLinearLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLinearLayout.java
@@ -79,7 +79,6 @@
         a.recycle();
     }
 
-
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // This is essentially a bottom-up linear layout that only adds children that fit entirely
@@ -95,7 +94,10 @@
                 || getMeasuredHeight() != targetHeight
                 || mLastMeasuredWidth != widthSize;
 
+        // Now that we know which views to take, fix up the indents and see what width we get.
+        int measuredWidth = mPaddingLeft + mPaddingRight;
         final int count = getChildCount();
+        int totalHeight = getMeasuredHeight();
         if (recalculateVisibility) {
             // We only need to recalculate the view visibilities if the view wasn't measured already
             // in this pass, otherwise we may drop messages here already since we are measured
@@ -107,7 +109,7 @@
                 lp.hide = true;
             }
 
-            int totalHeight = mPaddingTop + mPaddingBottom;
+            totalHeight = mPaddingTop + mPaddingBottom;
             boolean first = true;
 
             // Starting from the bottom: we measure every view as if it were the only one. If it still
@@ -119,14 +121,6 @@
                 }
                 final View child = getChildAt(i);
                 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
-                ImageFloatingTextView textChild = null;
-                if (child instanceof ImageFloatingTextView) {
-                    // Pretend we need the image padding for all views, we don't know which
-                    // one will end up needing to do this (might end up not using all the space,
-                    // but calculating this exactly would be more expensive).
-                    textChild = (ImageFloatingTextView) child;
-                    textChild.setNumIndentLines(mIndentLines == 2 ? 3 : mIndentLines);
-                }
 
                 int spacing = first ? 0 : mSpacing;
                 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight
@@ -136,67 +130,28 @@
                 int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin +
                         lp.bottomMargin + spacing);
                 first = false;
-                boolean measuredTooSmall = false;
-                if (textChild != null) {
-                    measuredTooSmall = childHeight < textChild.getLayoutHeight()
-                            + textChild.getPaddingTop() + textChild.getPaddingBottom();
+                int measureType = MessagingChild.MEASURED_NORMAL;
+                if (child instanceof MessagingChild) {
+                    measureType = ((MessagingChild) child).getMeasuredType();
+                    linesRemaining -= messagingChild.getConsumedLines();
                 }
-
-                if (newHeight <= targetHeight && !measuredTooSmall) {
+                boolean isShortened = measureType == MessagingChild.MEASURED_SHORTENED;
+                boolean isTooSmall = measureType == MessagingChild.MEASURED_TOO_SMALL;
+                if (newHeight <= targetHeight && !isTooSmall) {
                     totalHeight = newHeight;
+                    measuredWidth = Math.max(measuredWidth,
+                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin
+                                    + mPaddingLeft + mPaddingRight);
                     lp.hide = false;
+                    if (isShortened) {
+                        break;
+                    }
                 } else {
                     break;
                 }
             }
         }
 
-        // Now that we know which views to take, fix up the indents and see what width we get.
-        int measuredWidth = mPaddingLeft + mPaddingRight;
-        int imageLines = mIndentLines;
-        // Need to redo the height because it may change due to changing indents.
-        int totalHeight = mPaddingTop + mPaddingBottom;
-        boolean first = true;
-        for (int i = 0; i < count; i++) {
-            final View child = getChildAt(i);
-            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
-
-            if (child.getVisibility() == GONE || lp.hide) {
-                continue;
-            }
-
-            if (child instanceof ImageFloatingTextView) {
-                ImageFloatingTextView textChild = (ImageFloatingTextView) child;
-                if (imageLines == 2 && textChild.getLineCount() > 2) {
-                    // HACK: If we need indent for two lines, and they're coming from the same
-                    // view, we need extra spacing to compensate for the lack of margins,
-                    // so add an extra line of indent.
-                    imageLines = 3;
-                }
-                boolean changed = textChild.setNumIndentLines(Math.max(0, imageLines));
-                if (changed || !recalculateVisibility) {
-                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
-                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
-                            lp.width);
-                    // we want to measure it at most as high as it is currently, otherwise we'll
-                    // drop later lines
-                    final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
-                            targetHeight - child.getMeasuredHeight(), lp.height);
-
-                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);;
-                }
-                imageLines -= textChild.getLineCount();
-            }
-
-            measuredWidth = Math.max(measuredWidth,
-                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin
-                            + mPaddingLeft + mPaddingRight);
-            totalHeight = Math.max(totalHeight, totalHeight + child.getMeasuredHeight() +
-                    lp.topMargin + lp.bottomMargin + (first ? 0 : mSpacing));
-            first = false;
-        }
-
-
         setMeasuredDimension(
                 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
                         widthMeasureSpec),
@@ -287,23 +242,18 @@
      * Sets how many lines should be indented to avoid a floating image.
      */
     @RemotableViewMethod
-    public void setNumIndentLines(int numberLines) {
+    public boolean setNumIndentLines(int numberLines) {
+        boolean changed = numberLines != mIndentLines;
         mIndentLines = numberLines;
+        return changed;
     }
 
-    /**
-     * Set id of the child that's also visible in the contracted layout.
-     */
-    @RemotableViewMethod
-    public void setContractedChildId(int contractedChildId) {
-        mContractedChildId = contractedChildId;
-    }
-
-    /**
-     * Get id of the child that's also visible in the contracted layout.
-     */
-    public int getContractedChildId() {
-        return mContractedChildId;
+    public interface MessagingChild {
+        int MEASURED_NORMAL = 0;
+        int MEASURED_SHORTENED = 1;
+        int MEASURED_TOO_SMALL = 2;
+        int getMeasuredType();
+        int getConsumedLines();
     }
 
     public static class LayoutParams extends MarginLayoutParams {
diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java
new file mode 100644
index 0000000..bedacf9
--- /dev/null
+++ b/core/java/com/android/internal/widget/MessagingMessage.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017 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.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.app.Notification;
+import android.content.Context;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Pools;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+
+import com.android.internal.R;
+
+import java.util.Objects;
+
+/**
+ * A message of a {@link MessagingLayout}.
+ */
+@RemoteViews.RemoteView
+public class MessagingMessage extends ImageFloatingTextView implements
+        MessagingLinearLayout.MessagingChild {
+
+    private static Pools.SimplePool<MessagingMessage> sInstancePool
+            = new Pools.SynchronizedPool<>(10);
+    private Notification.MessagingStyle.Message mMessage;
+    private MessagingGroup mGroup;
+    private boolean mIsHistoric;
+
+    public MessagingMessage(@NonNull Context context) {
+        super(context);
+    }
+
+    public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public MessagingMessage(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void setMessage(Notification.MessagingStyle.Message message) {
+        mMessage = message;
+        setText(message.getText());
+    }
+
+    public Notification.MessagingStyle.Message getMessage() {
+        return mMessage;
+    }
+
+    boolean sameAs(Notification.MessagingStyle.Message message) {
+        if (!Objects.equals(message.getText(), mMessage.getText())) {
+            return false;
+        }
+        if (!Objects.equals(message.getSender(), mMessage.getSender())) {
+            return false;
+        }
+        if (!Objects.equals(message.getTimestamp(), mMessage.getTimestamp())) {
+            return false;
+        }
+        return true;
+    }
+
+    static MessagingMessage createMessage(MessagingLayout layout,
+            Notification.MessagingStyle.Message m) {
+        MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout();
+        MessagingMessage createdMessage = sInstancePool.acquire();
+        if (createdMessage == null) {
+            createdMessage = (MessagingMessage) LayoutInflater.from(layout.getContext()).inflate(
+                    R.layout.notification_template_messaging_message, messagingLinearLayout,
+                    false);
+        }
+        createdMessage.setMessage(m);
+        return createdMessage;
+    }
+
+    public void removeMessage() {
+        mGroup.removeMessage(this);
+        mGroup = null;
+        mMessage = null;
+        sInstancePool.release(this);
+    }
+
+    public void setMessagingGroup(MessagingGroup group) {
+        mGroup = group;
+    }
+
+    public static void dropCache() {
+        sInstancePool = new Pools.SynchronizedPool<>(10);
+    }
+
+    public void setIsHistoric(boolean isHistoric) {
+        mIsHistoric = isHistoric;
+    }
+
+    public MessagingGroup getGroup() {
+        return mGroup;
+    }
+
+    @Override
+    public int getMeasuredType() {
+        boolean measuredTooSmall = getMeasuredHeight()
+                < getLayoutHeight() + getPaddingTop() + getPaddingBottom();
+        if (measuredTooSmall) {
+            return MEASURED_TOO_SMALL;
+        } else {
+            Layout layout = getLayout();
+            if (layout == null) {
+                return MEASURED_TOO_SMALL;
+            }
+            if (layout.getEllipsisCount(layout.getLineCount() - 1) > 0) {
+                return MEASURED_SHORTENED;
+            } else {
+                return MEASURED_NORMAL;
+            }
+        }
+    }
+
+    @Override
+    public int getConsumedLines() {
+        return getLineCount();
+    }
+
+    public int getLayoutHeight() {
+        Layout layout = getLayout();
+        if (layout == null) {
+            return 0;
+        }
+        return layout.getHeight();
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/core/res/res/drawable/ic_reply_notification_large.xml b/core/res/res/drawable/ic_reply_notification_large.xml
new file mode 100644
index 0000000..e75afddf
--- /dev/null
+++ b/core/res/res/drawable/ic_reply_notification_large.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       android:inset="8dp">
+    <vector android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24.0"
+            android:viewportWidth="24.0">
+        <path
+            android:fillColor="#FFFFFFFF"
+            android:pathData="M10.0,9.0L10.0,5.0l-7.0,7.0 7.0,7.0l0.0,-4.1c5.0,0.0 8.5,1.6 11.0,5.1 -1.0,-5.0 -4.0,-10.0 -11.0,-11.0z"/>
+        <path
+            android:fillColor="#00000000"
+            android:pathData="M0 0h24v24H0z"/>
+    </vector>
+</inset>
diff --git a/core/res/res/drawable/messaging_message_background.xml b/core/res/res/drawable/messaging_message_background.xml
new file mode 100644
index 0000000..8a2096a
--- /dev/null
+++ b/core/res/res/drawable/messaging_message_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle"
+    android:tint="#14000000">
+    <corners android:radius="4dp" />
+    <padding android:bottom="6dp"
+        android:left="8dp"
+        android:right="8dp"
+        android:top="6dp" />
+</shape>
diff --git a/core/res/res/layout/notification_template_material_messaging.xml b/core/res/res/layout/notification_template_material_messaging.xml
index fd5154a..76b4c81 100644
--- a/core/res/res/layout/notification_template_material_messaging.xml
+++ b/core/res/res/layout/notification_template_material_messaging.xml
@@ -14,13 +14,17 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.internal.widget.MessagingLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:tag="messaging"
     >
-    <include layout="@layout/notification_template_header" />
+    <include layout="@layout/notification_template_header"
+             android:layout_width="wrap_content"
+             android:layout_height="@dimen/notification_header_height"
+             android:layout_marginEnd="56dp"/>
     <LinearLayout
             android:id="@+id/notification_action_list_margin_target"
             android:layout_width="match_parent"
@@ -50,31 +54,14 @@
                 android:id="@+id/notification_messaging"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:spacing="@dimen/notification_messaging_spacing" >
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text0"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text1"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text2"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text3"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text4"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text5"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-                <com.android.internal.widget.ImageFloatingTextView android:id="@+id/inbox_text6"
-                    style="@style/Widget.Material.Notification.MessagingText"
-                    />
-            </com.android.internal.widget.MessagingLinearLayout>
+                android:layout_marginTop="2dp"
+                android:spacing="@dimen/notification_messaging_spacing" />
         </LinearLayout>
     </LinearLayout>
     <include layout="@layout/notification_material_action_list" />
-    <include layout="@layout/notification_template_right_icon" />
-</FrameLayout>
+    <include layout="@layout/notification_template_right_icon"
+             android:layout_width="wrap_content"
+             android:layout_height="wrap_content"
+             android:layout_marginTop="18dp"
+             android:layout_gravity="top|end"/>
+</com.android.internal.widget.MessagingLayout>
diff --git a/core/res/res/layout/notification_template_messaging_group.xml b/core/res/res/layout/notification_template_messaging_group.xml
new file mode 100644
index 0000000..5adf7a3
--- /dev/null
+++ b/core/res/res/layout/notification_template_messaging_group.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 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.internal.widget.MessagingGroup
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/status_bar_latest_event_content"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal" >
+    <ImageView
+        android:id="@+id/message_icon"
+        android:layout_width="@dimen/messaging_avatar_size"
+        android:layout_height="@dimen/messaging_avatar_size"
+        android:layout_marginEnd="8dp"
+        android:scaleType="centerCrop"
+        android:importantForAccessibility="no" />
+    <LinearLayout
+        android:id="@+id/message_group_and_sender_container"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+        <com.android.internal.widget.MessagingLinearLayout
+            android:id="@+id/group_message_container"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:spacing="2dp"
+            android:layout_weight="1"/>
+        <com.android.internal.widget.ImageFloatingTextView
+            android:id="@+id/message_name"
+            style="@style/Widget.Material.Notification.MessagingName"
+            android:layout_width="wrap_content"
+            android:paddingStart="8dp"
+            android:paddingEnd="8dp"
+            android:paddingTop="2dp"
+        />
+    </LinearLayout>
+</com.android.internal.widget.MessagingGroup>
diff --git a/core/res/res/layout/notification_template_messaging_message.xml b/core/res/res/layout/notification_template_messaging_message.xml
new file mode 100644
index 0000000..ab6466c
--- /dev/null
+++ b/core/res/res/layout/notification_template_messaging_message.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.internal.widget.MessagingMessage
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/message_text"
+    style="@style/Widget.Material.Notification.MessagingText"
+/>
diff --git a/core/res/res/layout/notification_template_right_icon.xml b/core/res/res/layout/notification_template_right_icon.xml
index d379256..8fb2887 100644
--- a/core/res/res/layout/notification_template_right_icon.xml
+++ b/core/res/res/layout/notification_template_right_icon.xml
@@ -19,12 +19,12 @@
              android:id="@+id/right_icon_container"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
+             android:layout_marginTop="36dp"
              android:layout_gravity="top|end">
     <ImageView android:id="@+id/right_icon"
                android:layout_width="@dimen/notification_right_icon_size"
                android:layout_height="@dimen/notification_right_icon_size"
                android:layout_gravity="top|end"
-               android:layout_marginTop="36dp"
                android:layout_marginEnd="@dimen/notification_content_margin_end"
                android:scaleType="centerCrop"
                android:importantForAccessibility="no" />
@@ -32,7 +32,7 @@
                android:layout_width="16dp"
                android:layout_height="16dp"
                android:layout_gravity="top|end"
-               android:layout_marginTop="64dp"
+               android:layout_marginTop="28dp"
                android:layout_marginEnd="12dp"
                android:background="@drawable/notification_reply_background"
                android:src="@drawable/ic_reply_notification"
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 947fcf1..946216c 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -605,6 +605,8 @@
     <!-- The size of the right icon image when on low ram -->
     <dimen name="notification_right_icon_size_low_ram">40dp</dimen>
 
+    <dimen name="messaging_avatar_size">24dp</dimen>
+
     <!-- Max width/height of the autofill data set picker as a fraction of the screen width/height -->
     <dimen name="autofill_dataset_picker_max_size">90%</dimen>
 
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index cddf99a..2cd4dcb 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -505,8 +505,17 @@
         <item name="layout_width">match_parent</item>
         <item name="layout_height">wrap_content</item>
         <item name="ellipsize">end</item>
-        <item name="visibility">gone</item>
         <item name="textAppearance">@style/TextAppearance.Material.Notification</item>
+        <item name="background">@drawable/messaging_message_background</item>
+    </style>
+
+    <style name="Widget.Material.Notification.MessagingName" parent="Widget.Material.Light.TextView">
+        <item name="layout_width">wrap_content</item>
+        <item name="layout_height">wrap_content</item>
+        <item name="ellipsize">end</item>
+        <item name="textAppearance">@style/TextAppearance.Material.Notification</item>
+        <item name="textColor">@color/notification_primary_text_color_light</item>
+        <item name="textSize">12sp</item>
     </style>
 
     <!-- Widget Styles -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 32758e8..abca5e5 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3105,6 +3105,15 @@
   <java-symbol type="dimen" name="chooser_service_spacing" />
   <java-symbol type="bool" name="config_showSysuiShutdown" />
 
+  <java-symbol type="layout" name="notification_template_messaging_message" />
+  <java-symbol type="layout" name="notification_template_messaging_group" />
+  <java-symbol type="id" name="message_text" />
+  <java-symbol type="id" name="message_name" />
+  <java-symbol type="id" name="message_icon" />
+  <java-symbol type="id" name="group_message_container" />
+  <java-symbol type="drawable" name="ic_reply_notification_large" />
+  <java-symbol type="dimen" name="messaging_avatar_size" />
+
   <java-symbol type="integer" name="config_stableDeviceDisplayWidth" />
   <java-symbol type="integer" name="config_stableDeviceDisplayHeight" />
   <java-symbol type="bool" name="config_display_no_service_when_sim_unready" />
diff --git a/core/tests/coretests/src/com/android/internal/widget/MessagingLinearLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/MessagingLinearLayoutTest.java
index dfe8511..3919fdd 100644
--- a/core/tests/coretests/src/com/android/internal/widget/MessagingLinearLayoutTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/MessagingLinearLayoutTest.java
@@ -23,11 +23,11 @@
 import android.content.Context;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
+import android.text.Layout;
 import android.view.LayoutInflater;
 import android.view.View.MeasureSpec;
 
 import com.android.frameworks.coretests.R;
-import com.google.common.base.Function;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,33 +52,28 @@
 
     @Test
     public void testSingleChild() {
-        FakeImageFloatingTextView child = fakeChild((i) -> 3);
+        FakeImageFloatingTextView child = fakeChild(3);
 
-        mView.setNumIndentLines(2);
         mView.addView(child);
 
         mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
         mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
 
-        assertEquals(3, child.getNumIndentLines());
         assertFalse(child.isHidden());
         assertEquals(150, mView.getMeasuredHeight());
     }
 
     @Test
     public void testLargeSmall() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> 3);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 1);
+        FakeImageFloatingTextView child1 = fakeChild(3);
+        FakeImageFloatingTextView child2 = fakeChild(1);
 
-        mView.setNumIndentLines(2);
         mView.addView(child1);
         mView.addView(child2);
 
         mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
         mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
 
-        assertEquals(3, child1.getNumIndentLines());
-        assertEquals(0, child2.getNumIndentLines());
         assertFalse("child1 should not be hidden", child1.isHidden());
         assertFalse("child2 should not be hidden", child2.isHidden());
         assertEquals(205, mView.getMeasuredHeight());
@@ -86,18 +81,15 @@
 
     @Test
     public void testSmallSmall() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> 1);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 1);
+        FakeImageFloatingTextView child1 = fakeChild(1);
+        FakeImageFloatingTextView child2 = fakeChild(1);
 
-        mView.setNumIndentLines(2);
         mView.addView(child1);
         mView.addView(child2);
 
         mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
         mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
 
-        assertEquals(2, child1.getNumIndentLines());
-        assertEquals(1, child2.getNumIndentLines());
         assertFalse("child1 should not be hidden", child1.isHidden());
         assertFalse("child2 should not be hidden", child2.isHidden());
         assertEquals(105, mView.getMeasuredHeight());
@@ -105,17 +97,15 @@
 
     @Test
     public void testLargeLarge() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> 7);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 7);
+        FakeImageFloatingTextView child1 = fakeChild(7);
+        FakeImageFloatingTextView child2 = fakeChild(7);
 
-        mView.setNumIndentLines(2);
         mView.addView(child1);
         mView.addView(child2);
 
         mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
         mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
 
-        assertEquals(3, child2.getNumIndentLines());
         assertTrue("child1 should be hidden", child1.isHidden());
         assertFalse("child2 should not be hidden", child2.isHidden());
         assertEquals(350, mView.getMeasuredHeight());
@@ -123,10 +113,9 @@
 
     @Test
     public void testLargeSmall_largeWrapsWith3indentbutNotFullHeight_andHitsMax() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> i > 2 ? 7 : 6);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 1);
+        FakeImageFloatingTextView child1 = fakeChild(7);
+        FakeImageFloatingTextView child2 = fakeChild(1);
 
-        mView.setNumIndentLines(2);
         mView.addView(child1);
         mView.addView(child2);
 
@@ -135,51 +124,18 @@
 
         assertFalse("child1 should not be hidden", child1.isHidden());
         assertFalse("child2 should not be hidden", child2.isHidden());
-        assertEquals(355, mView.getMeasuredHeight());
-        assertEquals(3, child1.getNumIndentLines());
-        assertEquals(0, child2.getNumIndentLines());
+        assertEquals(355, mView.getMeasuredHeight());;
     }
 
-    @Test
-    public void testLargeSmall_largeWrapsWith3indentbutnot3() {
-        FakeImageFloatingTextView child1 = fakeChild((i) -> i > 2 ? 4 : 3);
-        FakeImageFloatingTextView child2 = fakeChild((i) -> 1);
-
-        mView.setNumIndentLines(2);
-        mView.addView(child1);
-        mView.addView(child2);
-
-        mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
-        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
-
-        assertFalse("child1 should not be hidden", child1.isHidden());
-        assertFalse("child2 should not be hidden", child2.isHidden());
-        assertEquals(255, mView.getMeasuredHeight());
-        assertEquals(3, child1.getNumIndentLines());
-        assertEquals(0, child2.getNumIndentLines());
-    }
-
-    private class FakeImageFloatingTextView extends ImageFloatingTextView {
+    private class FakeImageFloatingTextView extends MessagingMessage {
 
         public static final int LINE_HEIGHT = 50;
-        private final Function<Integer, Integer> mLinesForIndent;
-        private int mNumIndentLines;
+        private final int mNumLines;
 
         public FakeImageFloatingTextView(Context context,
-                Function<Integer, Integer> linesForIndent) {
+                int linesForIndent) {
             super(context, null, 0, 0);
-            mLinesForIndent = linesForIndent;
-        }
-
-        @Override
-        public boolean setNumIndentLines(int lines) {
-            boolean changed = (mNumIndentLines != lines);
-            mNumIndentLines = lines;
-            return changed;
-        }
-
-        public int getNumIndentLines() {
-            return mNumIndentLines;
+            mNumLines = linesForIndent;
         }
 
         @Override
@@ -195,6 +151,20 @@
                             heightMeasureSpec)));
         }
 
+        public int getMeasuredType() {
+            boolean measuredTooSmall = getMeasuredHeight()
+                    < getLayoutHeight() + getPaddingTop() + getPaddingBottom();
+            if (measuredTooSmall) {
+                return MEASURED_TOO_SMALL;
+            } else {
+                if (getMeasuredHeight() == getDesiredHeight()) {
+                    return MEASURED_NORMAL;
+                } else {
+                    return MEASURED_SHORTENED;
+                }
+            }
+        }
+
         private int clampToMultiplesOfLineHeight(int size) {
             if (size <= LINE_HEIGHT) {
                 return size;
@@ -204,7 +174,7 @@
 
         @Override
         public int getLineCount() {
-            return mLinesForIndent.apply(mNumIndentLines);
+            return mNumLines;
         }
 
         public int getDesiredHeight() {
@@ -229,7 +199,7 @@
         }
     }
 
-    private FakeImageFloatingTextView fakeChild(Function<Integer,Integer> linesForIndent) {
-        return new FakeImageFloatingTextView(mContext, linesForIndent);
+    private FakeImageFloatingTextView fakeChild(int numLines) {
+        return new FakeImageFloatingTextView(mContext, numLines);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
index f6ee1ca..d610492 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationMessagingTemplateViewWrapper.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification;
 
+import com.android.internal.widget.MessagingLayout;
 import com.android.internal.widget.MessagingLinearLayout;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.TransformableView;
@@ -32,41 +33,18 @@
  */
 public class NotificationMessagingTemplateViewWrapper extends NotificationTemplateViewWrapper {
 
+    private MessagingLayout mMessagingLayout;
     private View mContractedMessage;
-    private ArrayList<View> mHistoricMessages = new ArrayList<View>();
 
     protected NotificationMessagingTemplateViewWrapper(Context ctx, View view,
             ExpandableNotificationRow row) {
         super(ctx, view, row);
+        mMessagingLayout = (MessagingLayout) view;
     }
 
     private void resolveViews() {
         mContractedMessage = null;
-
-        View container = mView.findViewById(com.android.internal.R.id.notification_messaging);
-        if (container instanceof MessagingLinearLayout
-                && ((MessagingLinearLayout) container).getChildCount() > 0) {
-            MessagingLinearLayout messagingContainer = (MessagingLinearLayout) container;
-
-            int childCount = messagingContainer.getChildCount();
-            for (int i = 0; i < childCount; i++) {
-                View child = messagingContainer.getChildAt(i);
-
-                if (child.getVisibility() == View.GONE
-                        && child instanceof TextView
-                        && !TextUtils.isEmpty(((TextView) child).getText())) {
-                    mHistoricMessages.add(child);
-                }
-
-                // Only consider the first visible child - transforming to a position other than the
-                // first looks bad because we have to move across other messages that are fading in.
-                if (child.getId() == messagingContainer.getContractedChildId()) {
-                    mContractedMessage = child;
-                } else if (child.getVisibility() == View.VISIBLE) {
-                    break;
-                }
-            }
-        }
+        mContractedMessage = mMessagingLayout.getContractedMessage();
     }
 
     @Override
@@ -89,8 +67,6 @@
 
     @Override
     public void setRemoteInputVisible(boolean visible) {
-        for (int i = 0; i < mHistoricMessages.size(); i++) {
-            mHistoricMessages.get(i).setVisibility(visible ? View.VISIBLE : View.GONE);
-        }
+        mMessagingLayout.showHistoricMessages(visible);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index fa34d4a..5b1e261 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -146,6 +146,8 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.NotificationMessagingUtil;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.MessagingGroup;
+import com.android.internal.widget.MessagingMessage;
 import com.android.keyguard.KeyguardHostView.OnDismissAction;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
@@ -272,7 +274,7 @@
     public static final boolean ENABLE_CHILD_NOTIFICATIONS
             = SystemProperties.getBoolean("debug.child_notifs", true);
     public static final boolean FORCE_REMOTE_INPUT_HISTORY =
-            SystemProperties.getBoolean("debug.force_remoteinput_history", false);
+            SystemProperties.getBoolean("debug.force_remoteinput_history", true);
     private static final boolean ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT = false;
 
     protected static final int MSG_HIDE_RECENT_APPS = 1020;
@@ -1230,6 +1232,8 @@
     }
 
     public void onDensityOrFontScaleChanged() {
+        MessagingMessage.dropCache();
+        MessagingGroup.dropCache();
         // start old BaseStatusBar.onDensityOrFontScaleChanged().
         if (!KeyguardUpdateMonitor.getInstance(mContext).isSwitchingUser()) {
             updateNotificationsOnDensityOrFontScaleChanged();
@@ -1683,8 +1687,9 @@
             clearCurrentMediaNotification();
             updateMediaMetaData(true, true);
         }
-        if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputController.isSpinning(key)) {
-            Entry entry = mNotificationData.get(key);
+        Entry entry = mNotificationData.get(key);
+        if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputController.isSpinning(key)
+                && entry.row != null && !entry.row.isDismissed()) {
             StatusBarNotification sbn = entry.notification;
 
             Notification.Builder b = Notification.Builder
@@ -1720,6 +1725,7 @@
                 deferRemoval = false;
             }
             if (updated) {
+                Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key);
                 mKeysKeptForRemoteInput.add(entry.key);
                 return;
             }
@@ -1729,7 +1735,6 @@
             mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
             return;
         }
-        Entry entry = mNotificationData.get(key);
 
         if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
                 && (entry.row != null && !entry.row.isDismissed())) {