Added compatibility support for MessagingStyle

Change-Id: Ide2eba4efb261b9f010633500364822ac65fbb34
Fixes: 29574622
diff --git a/compat/java/android/support/v4/app/NotificationCompat.java b/compat/java/android/support/v4/app/NotificationCompat.java
index 009b864..cb02f41 100644
--- a/compat/java/android/support/v4/app/NotificationCompat.java
+++ b/compat/java/android/support/v4/app/NotificationCompat.java
@@ -525,7 +525,7 @@
         public Notification build(Builder b, BuilderExtender extender) {
             Notification result = b.mNotification;
             result = NotificationCompatBase.add(result, b.mContext,
-                    b.mContentTitle, b.mContentText, b.mContentIntent, b.mFullScreenIntent);
+                    b.resolveTitle(), b.resolveText(), b.mContentIntent, b.mFullScreenIntent);
             // translate high priority requests into legacy flag
             if (b.mPriority > PRIORITY_DEFAULT) {
                 result.flags |= FLAG_HIGH_PRIORITY;
@@ -604,7 +604,7 @@
         @Override
         public Notification build(Builder b, BuilderExtender extender) {
             Notification notification = NotificationCompatHoneycomb.add(b.mContext, b.mNotification,
-                    b.mContentTitle, b.mContentText, b.mContentInfo, b.mTickerView,
+                    b.resolveTitle(), b.resolveText(), b.mContentInfo, b.mTickerView,
                     b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon);
             if (b.mContentView != null) {
                 notification.contentView = b.mContentView;
@@ -617,9 +617,9 @@
         @Override
         public Notification build(Builder b, BuilderExtender extender) {
             NotificationCompatIceCreamSandwich.Builder builder =
-                    new NotificationCompatIceCreamSandwich.Builder(
-                            b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
-                            b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
+                    new NotificationCompatIceCreamSandwich.Builder(b.mContext, b.mNotification,
+                            b.resolveTitle(), b.resolveText(), b.mContentInfo, b.mTickerView,
+                            b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
                             b.mProgressMax, b.mProgress, b.mProgressIndeterminate);
             return extender.build(b, builder);
         }
@@ -629,7 +629,7 @@
         @Override
         public Notification build(Builder b, BuilderExtender extender) {
             NotificationCompatJellybean.Builder builder = new NotificationCompatJellybean.Builder(
-                    b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
+                    b.mContext, b.mNotification, b.resolveTitle(), b.resolveText(), b.mContentInfo,
                     b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
                     b.mProgressMax, b.mProgress, b.mProgressIndeterminate,
                     b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras,
@@ -638,7 +638,10 @@
             addStyleToBuilderJellybean(builder, b.mStyle);
             Notification notification = extender.build(b, builder);
             if (b.mStyle != null) {
-                b.mStyle.addCompatExtras(getExtras(notification));
+                Bundle extras = getExtras(notification);
+                if (extras != null) {
+                    b.mStyle.addCompatExtras(extras);
+                }
             }
             return notification;
         }
@@ -697,7 +700,7 @@
         @Override
         public Notification build(Builder b, BuilderExtender extender) {
             NotificationCompatKitKat.Builder builder = new NotificationCompatKitKat.Builder(
-                    b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
+                    b.mContext, b.mNotification, b.resolveTitle(), b.resolveText(), b.mContentInfo,
                     b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
                     b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen,
                     b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly,
@@ -749,7 +752,7 @@
         @Override
         public Notification build(Builder b, BuilderExtender extender) {
             NotificationCompatApi20.Builder builder = new NotificationCompatApi20.Builder(
-                    b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
+                    b.mContext, b.mNotification, b.resolveTitle(), b.resolveText(), b.mContentInfo,
                     b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
                     b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen,
                     b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mPeople, b.mExtras,
@@ -807,7 +810,7 @@
         @Override
         public Notification build(Builder b, BuilderExtender extender) {
             NotificationCompatApi21.Builder builder = new NotificationCompatApi21.Builder(
-                    b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
+                    b.mContext, b.mNotification, b.resolveTitle(), b.resolveText(), b.mContentInfo,
                     b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
                     b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen,
                     b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mCategory,
@@ -897,8 +900,6 @@
                         bigPictureStyle.mPicture,
                         bigPictureStyle.mBigLargeIcon,
                         bigPictureStyle.mBigLargeIconSet);
-            } else if (style instanceof MessagingStyle) {
-                // TODO implement BigText fallback
             }
         }
     }
@@ -1788,6 +1789,25 @@
         public int getColor() {
             return mColor;
         }
+
+
+        /**
+         * @return the text of the notification
+         *
+         * @hide
+         */
+        protected CharSequence resolveText() {
+            return mContentText;
+        }
+
+        /**
+         * @return the title of the notification
+         *
+         * @hide
+         */
+        protected CharSequence resolveTitle() {
+            return mContentTitle;
+        }
     }
 
     /**
@@ -1967,8 +1987,9 @@
      * messages of varying types between any number of people.
      *
      * <br>
-     * If the platform does not provide large-format notifications, this method has no effect. The
-     * user will always see the normal notification view.
+     * In order to get a backwards compatible behavior, the app needs to use the v7 version of the
+     * notification builder together with this style, otherwise the user will see the normal
+     * notification view.
      * <br>
      * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like
      * so:
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
index 6215d46..278493e 100644
--- a/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
+++ b/v7/appcompat/src/android/support/v7/app/NotificationCompat.java
@@ -19,12 +19,21 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
 import android.os.Build;
 import android.support.v4.app.NotificationBuilderWithBuilderAccessor;
 import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.text.BidiFormatter;
 import android.support.v7.appcompat.R;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
 import android.widget.RemoteViews;
 
+import java.util.List;
+
 /**
  * An extension of {@link android.support.v4.app.NotificationCompat} which supports
  * {@link android.support.v7.app.NotificationCompat.MediaStyle},
@@ -40,7 +49,7 @@
             NotificationCompatImpl24.addDecoratedCustomViewStyle(builder);
         } else if (b.mStyle instanceof DecoratedMediaCustomViewStyle) {
             NotificationCompatImpl24.addDecoratedMediaCustomViewStyle(builder);
-        } else {
+        } else if (!(b.mStyle instanceof MessagingStyle)) {
             addStyleGetContentViewLollipop(builder, b);
         }
     }
@@ -76,12 +85,107 @@
                 setBackgroundColor(b.mContext, contentViewMedia, b.getColor());
                 return contentViewMedia;
             }
+            return null;
         } else if (b.mStyle instanceof DecoratedCustomViewStyle) {
             return getDecoratedContentView(b);
         }
+        return addStyleGetContentViewJellybean(builder, b);
+    }
+
+    private static RemoteViews addStyleGetContentViewJellybean(
+            NotificationBuilderWithBuilderAccessor builder,
+            android.support.v4.app.NotificationCompat.Builder b) {
+        if (b.mStyle instanceof MessagingStyle) {
+            addMessagingFallBackStyle((MessagingStyle) b.mStyle, builder, b);
+        }
+        return addStyleGetContentViewIcs(builder, b);
+    }
+
+    private static MessagingStyle.Message findLatestIncomingMessage(MessagingStyle style) {
+        List<MessagingStyle.Message> messages = style.getMessages();
+        for (int i = messages.size() - 1; i >= 0; i--) {
+            MessagingStyle.Message m = messages.get(i);
+            // Incoming messages have a non-empty sender.
+            if (!TextUtils.isEmpty(m.getSender())) {
+                return m;
+            }
+        }
+        if (!messages.isEmpty()) {
+            // No incoming messages, fall back to outgoing message
+            return messages.get(messages.size() - 1);
+        }
         return null;
     }
 
+    private static CharSequence makeMessageLine(android.support.v4.app.NotificationCompat.Builder b,
+            MessagingStyle style,
+            MessagingStyle.Message m) {
+        BidiFormatter bidi = BidiFormatter.getInstance();
+        SpannableStringBuilder sb = new SpannableStringBuilder();
+        boolean afterLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+        int color = afterLollipop || Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1
+                ? Color.BLACK : Color.WHITE;
+        CharSequence replyName = m.getSender();
+        if (TextUtils.isEmpty(m.getSender())) {
+            replyName = style.getUserDisplayName() == null
+                    ? "" : style.getUserDisplayName();
+            color = afterLollipop && b.getColor() != NotificationCompat.COLOR_DEFAULT
+                    ? b.getColor()
+                    : color;
+        }
+        CharSequence senderText = bidiWrapIfNotSpanned(bidi, replyName);
+        sb.append(senderText);
+        sb.setSpan(makeFontColorSpan(color),
+                sb.length() - senderText.length(),
+                sb.length(),
+                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE /* flags */);
+        CharSequence text = m.getText() == null ? "" : m.getText();
+        sb.append("  ").append(bidiWrapIfNotSpanned(bidi, text));
+        return sb;
+    }
+
+    private static CharSequence bidiWrapIfNotSpanned(BidiFormatter bidi, CharSequence replyName) {
+        // Unfortunately bidiFormatter doesn't support CharSequences in support
+        if (replyName instanceof Spanned) {
+            return replyName;
+        }
+        return bidi.unicodeWrap(replyName.toString());
+    }
+
+    private static TextAppearanceSpan makeFontColorSpan(int color) {
+        return new TextAppearanceSpan(null, 0, 0, ColorStateList.valueOf(color), null);
+    }
+
+    private static void addMessagingFallBackStyle(MessagingStyle style,
+            NotificationBuilderWithBuilderAccessor builder,
+            android.support.v4.app.NotificationCompat.Builder b) {
+        SpannableStringBuilder completeMessage = new SpannableStringBuilder();
+        List<MessagingStyle.Message> messages = style.getMessages();
+        boolean showNames = style.getConversationTitle() != null
+                || hasMessagesWithoutSender(style.getMessages());
+        for (int i = messages.size() - 1; i >= 0; i--) {
+            MessagingStyle.Message m = messages.get(i);
+            CharSequence line;
+            line = showNames ? makeMessageLine(b, style, m) : m.getText();
+            if (i != messages.size() - 1) {
+                completeMessage.insert(0, "\n");
+            }
+            completeMessage.insert(0, line);
+        }
+        NotificationCompatImplJellybean.addBigTextStyle(builder, completeMessage);
+    }
+
+    private static boolean hasMessagesWithoutSender(
+            List<MessagingStyle.Message> messages) {
+        for (int i = messages.size() - 1; i >= 0; i--) {
+            MessagingStyle.Message m = messages.get(i);
+            if (m.getSender() == null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static RemoteViews addStyleGetContentViewIcs(
             NotificationBuilderWithBuilderAccessor builder,
             android.support.v4.app.NotificationCompat.Builder b) {
@@ -238,6 +342,43 @@
         }
 
         /**
+         * @return the text of the notification
+         *
+         * @hide
+         */
+        @Override
+        protected CharSequence resolveText() {
+            if (mStyle instanceof MessagingStyle) {
+                MessagingStyle style = (MessagingStyle) mStyle;
+                MessagingStyle.Message m = findLatestIncomingMessage(style);
+                CharSequence conversationTitle = style.getConversationTitle();
+                if (m != null) {
+                    return conversationTitle != null ? makeMessageLine(this, style, m)
+                            : m.getText();
+                }
+            }
+            return super.resolveText();
+        }
+
+        /**
+         * @return the title of the notification
+         *
+         * @hide
+         */
+        @Override
+        protected CharSequence resolveTitle() {
+            if (mStyle instanceof MessagingStyle) {
+                MessagingStyle style = (MessagingStyle) mStyle;
+                MessagingStyle.Message m = findLatestIncomingMessage(style);
+                CharSequence conversationTitle = style.getConversationTitle();
+                if (conversationTitle != null || m != null) {
+                    return conversationTitle != null ? conversationTitle : m.getSender();
+                }
+            }
+            return super.resolveTitle();
+        }
+
+        /**
          * @hide
          */
         @Override
@@ -279,7 +420,7 @@
         @Override
         public Notification build(android.support.v4.app.NotificationCompat.Builder b,
                 NotificationBuilderWithBuilderAccessor builder) {
-            RemoteViews contentView = addStyleGetContentViewIcs(builder, b);
+            RemoteViews contentView = addStyleGetContentViewJellybean(builder, b);
             Notification n = builder.build();
             // The above call might override decorated content views again, let's make sure it
             // sticks.
diff --git a/v7/appcompat/src/android/support/v7/app/NotificationCompatImplJellybean.java b/v7/appcompat/src/android/support/v7/app/NotificationCompatImplJellybean.java
new file mode 100644
index 0000000..cf8d128
--- /dev/null
+++ b/v7/appcompat/src/android/support/v7/app/NotificationCompatImplJellybean.java
@@ -0,0 +1,29 @@
+/*
+ * 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
+ */
+
+package android.support.v7.app;
+
+import android.app.Notification;
+import android.support.v4.app.NotificationBuilderWithBuilderAccessor;
+
+class NotificationCompatImplJellybean {
+
+    public static void addBigTextStyle(NotificationBuilderWithBuilderAccessor b,
+            CharSequence bigText) {
+        Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle(b.getBuilder());
+        bigTextStyle.bigText(bigText);
+    }
+}