From b2f22f0f8c40847dbf8684fe691dd6fb4653382b Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Fri, 1 Feb 2019 16:17:28 +0100
Subject: [PATCH] Implemented more tests, added "view spoilers" feature

---
 app/build.gradle.kts                          |   7 +
 .../de/kuschku/quasseldroid/Quasseldroid.kt   |  10 +-
 .../service/QuasselNotificationBackend.kt     |   2 +-
 .../ui/chat/messages/MessageListFragment.kt   |  14 +-
 .../chat/messages/QuasselMessageRenderer.kt   |  10 +
 .../util/irc/format/ContentFormatter.kt       |  31 ++-
 .../util/irc/format/IrcFormatDeserializer.kt  |  62 +++---
 .../format/spans/IrcBackgroundColorSpan.kt    |  16 ++
 .../util/irc/format/spans/IrcBoldSpan.kt      |   1 +
 .../format/spans/IrcForegroundColorSpan.kt    |  18 ++
 .../util/irc/format/spans/IrcItalicSpan.kt    |   1 +
 .../util/irc/format/spans/IrcMonospaceSpan.kt |   1 +
 .../irc/format/spans/IrcStrikethroughSpan.kt  |   1 +
 .../util/irc/format/spans/IrcUnderlineSpan.kt |   1 +
 .../kuschku/quasseldroid/QuasseldroidTest.kt  |  26 +++
 .../irc/format/IrcFormatDeserializerTest.kt   | 202 ++++++++++++++++++
 gradle.properties                             |   2 +-
 17 files changed, 359 insertions(+), 46 deletions(-)
 create mode 100644 app/src/test/java/de/kuschku/quasseldroid/QuasseldroidTest.kt
 create mode 100644 app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 94f488ed1..1bc233e75 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -98,6 +98,10 @@ android {
     setTargetCompatibility(JavaVersion.VERSION_1_8)
   }
 
+  testOptions {
+    unitTests.isIncludeAndroidResources = true
+  }
+
   lintOptions {
     isWarningsAsErrors = true
     setLintConfig(file("../lint.xml"))
@@ -182,4 +186,7 @@ dependencies {
   }
 
   testImplementation("junit", "junit", "4.12")
+  testImplementation("org.robolectric", "robolectric", "4.1") {
+    exclude(group = "org.threeten", module = "threetenbp")
+  }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/Quasseldroid.kt b/app/src/main/java/de/kuschku/quasseldroid/Quasseldroid.kt
index 405b56856..a1c31e520 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/Quasseldroid.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/Quasseldroid.kt
@@ -38,12 +38,11 @@ import de.kuschku.quasseldroid.util.compatibility.AndroidLoggingHandler
 import de.kuschku.quasseldroid.util.compatibility.AndroidStreamChannelFactory
 import de.kuschku.quasseldroid.util.ui.LocaleHelper
 
-class Quasseldroid : DaggerApplication() {
+open class Quasseldroid : DaggerApplication() {
   override fun applicationInjector(): AndroidInjector<Quasseldroid> =
     DaggerAppComponent.builder().create(this)
 
-  override fun onCreate() {
-    super.onCreate()
+  protected open fun init() {
     if (LeakCanary.isInAnalyzerProcess(this)) {
       // This process is dedicated to LeakCanary for heap analysis.
       // You should not init your app in this process.
@@ -232,6 +231,11 @@ class Quasseldroid : DaggerApplication() {
     }
   }
 
+  override fun onCreate() {
+    super.onCreate()
+    init()
+  }
+
   override fun attachBaseContext(base: Context) {
     super.attachBaseContext(LocaleHelper.setLocale(base))
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
index c684f5a62..c3b1ae0e3 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt
@@ -351,7 +351,7 @@ class QuasselNotificationBackend @Inject constructor(
             selfColor = selfColor
           ))
         }
-        val content = contentFormatter.formatContent(it.content, false, it.networkId)
+        val content = contentFormatter.formatContent(it.content, false, false, it.networkId)
 
         NotificationMessage(
           messageId = it.messageId,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
index 2636c2c55..6e6ab231d 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt
@@ -250,6 +250,12 @@ class MessageListFragment : ServiceBoundFragment() {
           1    -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = true
           else -> actionMode?.menu?.findItem(R.id.action_user_info)?.isVisible = false
         }
+      } else {
+        val value = viewModel.expandedMessages.value
+        viewModel.expandedMessages.onNext(
+          if (value.contains(msg.original.messageId)) value - msg.original.messageId
+          else value + msg.original.messageId
+        )
       }
     }
     adapter.setOnLongClickListener { msg ->
@@ -289,14 +295,6 @@ class MessageListFragment : ServiceBoundFragment() {
     }
     adapter.setOnUrlLongClickListener(LinkLongClickMenuHelper())
 
-    adapter.setOnExpansionListener {
-      val value = viewModel.expandedMessages.value
-      viewModel.expandedMessages.onNext(
-        if (value.contains(it.messageId)) value - it.messageId
-        else value + it.messageId
-      )
-    }
-
     messageList.adapter = adapter
     messageList.layoutManager = linearLayoutManager
     messageList.itemAnimator = null
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
index 35b7214c4..10de33940 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt
@@ -259,6 +259,7 @@ class QuasselMessageRenderer @Inject constructor(
         }
         val content = contentFormatter.formatContent(message.content.content,
                                                      monochromeForeground,
+                                                     message.isExpanded,
                                                      message.content.networkId)
         val nickName = HostmaskHelper.nick(message.content.sender)
         val senderColorIndex = SenderColorUtil.senderColor(nickName)
@@ -317,6 +318,7 @@ class QuasselMessageRenderer @Inject constructor(
             contentFormatter.formatNick(message.content.sender, self, monochromeForeground, false),
             contentFormatter.formatContent(message.content.content,
                                            monochromeForeground,
+                                           message.isExpanded,
                                            message.content.networkId)
           ),
           avatarUrls = AvatarHelper.avatar(messageSettings, message.content, avatarSize),
@@ -337,6 +339,7 @@ class QuasselMessageRenderer @Inject constructor(
           contentFormatter.formatNick(message.content.sender, self, monochromeForeground, false),
           contentFormatter.formatContent(message.content.content,
                                          monochromeForeground,
+                                         message.isExpanded,
                                          message.content.networkId)
         ),
         hasDayChange = message.hasDayChange,
@@ -448,6 +451,7 @@ class QuasselMessageRenderer @Inject constructor(
             ),
             contentFormatter.formatContent(message.content.content,
                                            monochromeForeground,
+                                           message.isExpanded,
                                            message.content.networkId)
           )
         },
@@ -483,6 +487,7 @@ class QuasselMessageRenderer @Inject constructor(
             ),
             contentFormatter.formatContent(message.content.content,
                                            monochromeForeground,
+                                           message.isExpanded,
                                            message.content.networkId)
           )
         },
@@ -518,6 +523,7 @@ class QuasselMessageRenderer @Inject constructor(
                                           false),
               contentFormatter.formatContent(reason,
                                              monochromeForeground,
+                                             message.isExpanded,
                                              message.content.networkId)
             )
           },
@@ -554,6 +560,7 @@ class QuasselMessageRenderer @Inject constructor(
                                           false),
               contentFormatter.formatContent(reason,
                                              monochromeForeground,
+                                             message.isExpanded,
                                              message.content.networkId)
             )
           },
@@ -623,6 +630,7 @@ class QuasselMessageRenderer @Inject constructor(
         dayChange = formatDayChange(message),
         combined = contentFormatter.formatContent(message.content.content,
                                                   monochromeForeground,
+                                                  message.isExpanded,
                                                   message.content.networkId),
         hasDayChange = message.hasDayChange,
         isMarkerLine = message.isMarkerLine,
@@ -635,6 +643,7 @@ class QuasselMessageRenderer @Inject constructor(
         dayChange = formatDayChange(message),
         combined = contentFormatter.formatContent(message.content.content,
                                                   monochromeForeground,
+                                                  message.isExpanded,
                                                   message.content.networkId),
         hasDayChange = message.hasDayChange,
         isMarkerLine = message.isMarkerLine,
@@ -657,6 +666,7 @@ class QuasselMessageRenderer @Inject constructor(
         dayChange = formatDayChange(message),
         combined = contentFormatter.formatContent(message.content.content,
                                                   monochromeForeground,
+                                                  message.isExpanded,
                                                   message.content.networkId),
         hasDayChange = message.hasDayChange,
         isMarkerLine = message.isMarkerLine,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
index 392d0f126..116acfa76 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
@@ -24,10 +24,7 @@ import android.graphics.Typeface
 import android.text.SpannableString
 import android.text.Spanned
 import android.text.TextPaint
-import android.text.style.ClickableSpan
-import android.text.style.ForegroundColorSpan
-import android.text.style.StyleSpan
-import android.text.style.URLSpan
+import android.text.style.*
 import android.view.View
 import androidx.annotation.ColorInt
 import de.kuschku.libquassel.protocol.NetworkId
@@ -37,6 +34,8 @@ import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.MessageSettings
 import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.helper.styledAttributes
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcBackgroundColorSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcForegroundColorSpan
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import org.intellij.lang.annotations.Language
 import javax.inject.Inject
@@ -110,10 +109,34 @@ class ContentFormatter @Inject constructor(
 
   fun formatContent(content: String,
                     highlight: Boolean = false,
+                    showSpoilers: Boolean = false,
                     networkId: NetworkId?): CharSequence {
     val formattedText = ircFormatDeserializer.formatString(content, messageSettings.colorizeMirc)
     val text = SpannableString(formattedText)
 
+    if (showSpoilers) {
+      val spans = mutableMapOf<Triple<Int, Int, Int>, MutableList<Any>>()
+      for (span in text.getSpans(0, text.length, IrcForegroundColorSpan::class.java)) {
+        val from = text.getSpanStart(span)
+        val to = text.getSpanEnd(span)
+        spans.getOrPut(Triple(from, to, span.getForegroundColor()), ::mutableListOf).add(span)
+      }
+      for (span in text.getSpans(0, text.length, IrcBackgroundColorSpan::class.java)) {
+        val from = text.getSpanStart(span)
+        val to = text.getSpanEnd(span)
+        spans.getOrPut(Triple(from, to, span.getBackgroundColor()), ::mutableListOf).add(span)
+      }
+      for (group in spans.values) {
+        if (group.size > 1 &&
+            group.any { it is ForegroundColorSpan } &&
+            group.any { it is BackgroundColorSpan }) {
+          for (span in group) {
+            text.removeSpan(span)
+          }
+        }
+      }
+    }
+
     for (result in urlPattern.findAll(formattedText)) {
       val group = result.groups[1]
       if (group != null) {
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
index 9f12844d9..a390248ae 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
@@ -32,34 +32,37 @@ import javax.inject.Inject
  * A helper class to turn mIRC formatted Strings into Android’s SpannableStrings with the same
  * color and format codes
  */
-class IrcFormatDeserializer @Inject constructor(context: Context) {
-  private val mircColors = listOf(
-    R.color.mircColor00, R.color.mircColor01, R.color.mircColor02, R.color.mircColor03,
-    R.color.mircColor04, R.color.mircColor05, R.color.mircColor06, R.color.mircColor07,
-    R.color.mircColor08, R.color.mircColor09, R.color.mircColor10, R.color.mircColor11,
-    R.color.mircColor12, R.color.mircColor13, R.color.mircColor14, R.color.mircColor15,
-    R.color.mircColor16, R.color.mircColor17, R.color.mircColor18, R.color.mircColor19,
-    R.color.mircColor20, R.color.mircColor21, R.color.mircColor22, R.color.mircColor23,
-    R.color.mircColor24, R.color.mircColor25, R.color.mircColor26, R.color.mircColor27,
-    R.color.mircColor28, R.color.mircColor29, R.color.mircColor30, R.color.mircColor31,
-    R.color.mircColor32, R.color.mircColor33, R.color.mircColor34, R.color.mircColor35,
-    R.color.mircColor36, R.color.mircColor37, R.color.mircColor38, R.color.mircColor39,
-    R.color.mircColor40, R.color.mircColor41, R.color.mircColor42, R.color.mircColor43,
-    R.color.mircColor44, R.color.mircColor45, R.color.mircColor46, R.color.mircColor47,
-    R.color.mircColor48, R.color.mircColor49, R.color.mircColor50, R.color.mircColor51,
-    R.color.mircColor52, R.color.mircColor53, R.color.mircColor54, R.color.mircColor55,
-    R.color.mircColor56, R.color.mircColor57, R.color.mircColor58, R.color.mircColor59,
-    R.color.mircColor60, R.color.mircColor61, R.color.mircColor62, R.color.mircColor63,
-    R.color.mircColor64, R.color.mircColor65, R.color.mircColor66, R.color.mircColor67,
-    R.color.mircColor68, R.color.mircColor69, R.color.mircColor70, R.color.mircColor71,
-    R.color.mircColor72, R.color.mircColor73, R.color.mircColor74, R.color.mircColor75,
-    R.color.mircColor76, R.color.mircColor77, R.color.mircColor78, R.color.mircColor79,
-    R.color.mircColor80, R.color.mircColor81, R.color.mircColor82, R.color.mircColor83,
-    R.color.mircColor84, R.color.mircColor85, R.color.mircColor86, R.color.mircColor87,
-    R.color.mircColor88, R.color.mircColor89, R.color.mircColor90, R.color.mircColor91,
-    R.color.mircColor92, R.color.mircColor93, R.color.mircColor94, R.color.mircColor95,
-    R.color.mircColor96, R.color.mircColor97, R.color.mircColor98
-  ).map(context::getColorCompat).toIntArray()
+class IrcFormatDeserializer(private val mircColors: IntArray) {
+  @Inject
+  constructor(context: Context) : this(
+    mircColors = listOf(
+      R.color.mircColor00, R.color.mircColor01, R.color.mircColor02, R.color.mircColor03,
+      R.color.mircColor04, R.color.mircColor05, R.color.mircColor06, R.color.mircColor07,
+      R.color.mircColor08, R.color.mircColor09, R.color.mircColor10, R.color.mircColor11,
+      R.color.mircColor12, R.color.mircColor13, R.color.mircColor14, R.color.mircColor15,
+      R.color.mircColor16, R.color.mircColor17, R.color.mircColor18, R.color.mircColor19,
+      R.color.mircColor20, R.color.mircColor21, R.color.mircColor22, R.color.mircColor23,
+      R.color.mircColor24, R.color.mircColor25, R.color.mircColor26, R.color.mircColor27,
+      R.color.mircColor28, R.color.mircColor29, R.color.mircColor30, R.color.mircColor31,
+      R.color.mircColor32, R.color.mircColor33, R.color.mircColor34, R.color.mircColor35,
+      R.color.mircColor36, R.color.mircColor37, R.color.mircColor38, R.color.mircColor39,
+      R.color.mircColor40, R.color.mircColor41, R.color.mircColor42, R.color.mircColor43,
+      R.color.mircColor44, R.color.mircColor45, R.color.mircColor46, R.color.mircColor47,
+      R.color.mircColor48, R.color.mircColor49, R.color.mircColor50, R.color.mircColor51,
+      R.color.mircColor52, R.color.mircColor53, R.color.mircColor54, R.color.mircColor55,
+      R.color.mircColor56, R.color.mircColor57, R.color.mircColor58, R.color.mircColor59,
+      R.color.mircColor60, R.color.mircColor61, R.color.mircColor62, R.color.mircColor63,
+      R.color.mircColor64, R.color.mircColor65, R.color.mircColor66, R.color.mircColor67,
+      R.color.mircColor68, R.color.mircColor69, R.color.mircColor70, R.color.mircColor71,
+      R.color.mircColor72, R.color.mircColor73, R.color.mircColor74, R.color.mircColor75,
+      R.color.mircColor76, R.color.mircColor77, R.color.mircColor78, R.color.mircColor79,
+      R.color.mircColor80, R.color.mircColor81, R.color.mircColor82, R.color.mircColor83,
+      R.color.mircColor84, R.color.mircColor85, R.color.mircColor86, R.color.mircColor87,
+      R.color.mircColor88, R.color.mircColor89, R.color.mircColor90, R.color.mircColor91,
+      R.color.mircColor92, R.color.mircColor93, R.color.mircColor94, R.color.mircColor95,
+      R.color.mircColor96, R.color.mircColor97, R.color.mircColor98
+    ).map(context::getColorCompat).toIntArray()
+  )
 
   /**
    * Function to handle mIRC formatted strings
@@ -274,6 +277,8 @@ class IrcFormatDeserializer @Inject constructor(context: Context) {
       i++
     }
 
+    plainText.append(str.substring(str.length - normalCount, str.length))
+
     // End all formatting tags
     if (bold != null) {
       if (colorize) bold.apply(plainText, plainText.length)
@@ -296,7 +301,6 @@ class IrcFormatDeserializer @Inject constructor(context: Context) {
     if (hexColor != null) {
       if (colorize) hexColor.apply(plainText, plainText.length)
     }
-    plainText.append(str.substring(str.length - normalCount, str.length))
     return plainText
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcBackgroundColorSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcBackgroundColorSpan.kt
index a7abfdf30..7e3647364 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcBackgroundColorSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcBackgroundColorSpan.kt
@@ -27,10 +27,26 @@ sealed class IrcBackgroundColorSpan<T : IrcBackgroundColorSpan<T>>(@ColorInt col
   class MIRC(private val mircColor: Int, @ColorInt color: Int) :
     IrcBackgroundColorSpan<MIRC>(color), Copyable<MIRC> {
     override fun copy() = MIRC(mircColor, backgroundColor)
+    override fun toString(): String {
+      return "IrcBackgroundColorSpan.MIRC(mircColor=$mircColor, color=${backgroundColor.toString(16)})"
+    }
+
+    override fun equals(other: Any?) = when (other) {
+      is IrcBackgroundColorSpan.MIRC -> other.mircColor == mircColor
+      else                           -> false
+    }
   }
 
   class HEX(@ColorInt color: Int) :
     IrcBackgroundColorSpan<HEX>(color), Copyable<HEX> {
     override fun copy() = HEX(backgroundColor)
+    override fun toString(): String {
+      return "IrcBackgroundColorSpan.HEX(color=${backgroundColor.toString(16)})"
+    }
+
+    override fun equals(other: Any?) = when (other) {
+      is IrcBackgroundColorSpan.HEX -> other.backgroundColor == backgroundColor
+      else                          -> false
+    }
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcBoldSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcBoldSpan.kt
index 1b77aa2c3..c7c15509c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcBoldSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcBoldSpan.kt
@@ -24,4 +24,5 @@ import android.text.style.StyleSpan
 
 class IrcBoldSpan : StyleSpan(Typeface.BOLD), Copyable<IrcBoldSpan> {
   override fun copy() = IrcBoldSpan()
+  override fun equals(other: Any?) = other is IrcBoldSpan
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcForegroundColorSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcForegroundColorSpan.kt
index 6e327b829..c5d440802 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcForegroundColorSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcForegroundColorSpan.kt
@@ -24,13 +24,31 @@ import androidx.annotation.ColorInt
 
 sealed class IrcForegroundColorSpan<T : IrcForegroundColorSpan<T>>(@ColorInt color: Int) :
   ForegroundColorSpan(color), Copyable<T> {
+
+
   class MIRC(private val mircColor: Int, @ColorInt color: Int) :
     IrcForegroundColorSpan<MIRC>(color), Copyable<MIRC> {
     override fun copy() = MIRC(mircColor, foregroundColor)
+    override fun toString(): String {
+      return "IrcForegroundColorSpan.MIRC(mircColor=$mircColor, color=${foregroundColor.toString(16)})"
+    }
+
+    override fun equals(other: Any?) = when (other) {
+      is MIRC -> other.mircColor == mircColor
+      else    -> false
+    }
   }
 
   class HEX(@ColorInt color: Int) :
     IrcForegroundColorSpan<HEX>(color), Copyable<HEX> {
     override fun copy() = HEX(foregroundColor)
+    override fun toString(): String {
+      return "IrcBackgroundColorSpan.HEX(color=${foregroundColor.toString(16)})"
+    }
+
+    override fun equals(other: Any?) = when (other) {
+      is HEX -> other.foregroundColor == foregroundColor
+      else   -> false
+    }
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcItalicSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcItalicSpan.kt
index 045a86918..da4bdf1a6 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcItalicSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcItalicSpan.kt
@@ -24,4 +24,5 @@ import android.text.style.StyleSpan
 
 class IrcItalicSpan : StyleSpan(Typeface.ITALIC), Copyable<IrcItalicSpan> {
   override fun copy() = IrcItalicSpan()
+  override fun equals(other: Any?) = other is IrcBoldSpan
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcMonospaceSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcMonospaceSpan.kt
index ffc54118a..ceee6d139 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcMonospaceSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcMonospaceSpan.kt
@@ -23,4 +23,5 @@ import android.text.style.TypefaceSpan
 
 class IrcMonospaceSpan : TypefaceSpan("monospace"), Copyable<IrcMonospaceSpan> {
   override fun copy() = IrcMonospaceSpan()
+  override fun equals(other: Any?) = other is IrcBoldSpan
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcStrikethroughSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcStrikethroughSpan.kt
index 4d2d336fb..3d6a2b6de 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcStrikethroughSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcStrikethroughSpan.kt
@@ -23,4 +23,5 @@ import android.text.style.StrikethroughSpan
 
 class IrcStrikethroughSpan : StrikethroughSpan(), Copyable<IrcStrikethroughSpan> {
   override fun copy() = IrcStrikethroughSpan()
+  override fun equals(other: Any?) = other is IrcBoldSpan
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcUnderlineSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcUnderlineSpan.kt
index 244c3ea3c..419db0439 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcUnderlineSpan.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/spans/IrcUnderlineSpan.kt
@@ -23,4 +23,5 @@ import android.text.style.UnderlineSpan
 
 class IrcUnderlineSpan : UnderlineSpan(), Copyable<IrcUnderlineSpan> {
   override fun copy() = IrcUnderlineSpan()
+  override fun equals(other: Any?) = other is IrcBoldSpan
 }
diff --git a/app/src/test/java/de/kuschku/quasseldroid/QuasseldroidTest.kt b/app/src/test/java/de/kuschku/quasseldroid/QuasseldroidTest.kt
new file mode 100644
index 000000000..5f0b57dc6
--- /dev/null
+++ b/app/src/test/java/de/kuschku/quasseldroid/QuasseldroidTest.kt
@@ -0,0 +1,26 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid
+
+class QuasseldroidTest : Quasseldroid() {
+  override fun init() {
+    applicationInjector().inject(this)
+  }
+}
diff --git a/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt b/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt
new file mode 100644
index 000000000..93e7a8575
--- /dev/null
+++ b/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.quasseldroid.util.irc.format
+
+import android.text.Spanned
+import android.text.SpannedString
+import de.kuschku.quasseldroid.QuasseldroidTest
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcBackgroundColorSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.IrcForegroundColorSpan
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@Config(application = QuasseldroidTest::class)
+@RunWith(RobolectricTestRunner::class)
+class IrcFormatDeserializerTest {
+  lateinit var deserializer: IrcFormatDeserializer
+
+  @Before
+  fun setUp() {
+    deserializer = IrcFormatDeserializer(mircColors = colors)
+  }
+
+  @Test
+  fun testMissingEndTag() {
+    val text = SpannedString.valueOf(deserializer.formatString(
+      "\u000301,01weeeeeeeeee",
+      colorize = true
+    ))
+    assertEquals(
+      listOf(
+        SpanInfo(
+          from = 0,
+          to = 11,
+          flags = Spanned.SPAN_INCLUSIVE_EXCLUSIVE,
+          span = IrcForegroundColorSpan.MIRC(mircColor = 1, color = colors[1])
+        ),
+        SpanInfo(
+          from = 0,
+          to = 11,
+          flags = Spanned.SPAN_INCLUSIVE_EXCLUSIVE,
+          span = IrcBackgroundColorSpan.MIRC(mircColor = 1, color = colors[1])
+        )
+      ),
+      text.allSpans<Any>()
+    )
+  }
+
+  inline fun <reified T> Spanned.allSpans(): List<SpanInfo> =
+    getSpans(0, length, T::class.java).map {
+      SpanInfo(
+        from = getSpanStart(it),
+        to = getSpanEnd(it),
+        flags = getSpanFlags(it),
+        span = it
+      )
+    }
+
+  data class SpanInfo(
+    val from: Int,
+    val to: Int,
+    val flags: Int,
+    val span: Any?
+  )
+
+  companion object {
+    val colors = intArrayOf(
+      0x00ffffff,
+      0x00000000,
+      0x00000080,
+      0x00008000,
+      0x00ff0000,
+      0x00800000,
+      0x00800080,
+      0x00ffa500,
+      0x00ffff00,
+      0x0000ff00,
+      0x00008080,
+      0x0000ffff,
+      0x004169e1,
+      0x00ff00ff,
+      0x00808080,
+      0x00c0c0c0,
+
+      0x00470000,
+      0x00740000,
+      0x00b50000,
+      0x00ff0000,
+      0x00ff5959,
+      0x00ff9c9c,
+
+      0x00472100,
+      0x00743a00,
+      0x00b56300,
+      0x00ff8c00,
+      0x00ffb459,
+      0x00ffd39c,
+
+      0x00474700,
+      0x00747400,
+      0x00b5b500,
+      0x00ffff00,
+      0x00ffff71,
+      0x00ffff9c,
+
+      0x00324700,
+      0x00517400,
+      0x007db500,
+      0x00b2ff00,
+      0x00cfff60,
+      0x00e2ff9c,
+
+      0x00004700,
+      0x00007400,
+      0x0000b500,
+      0x0000ff00,
+      0x006fff6f,
+      0x009cff9c,
+
+      0x0000472c,
+      0x00007449,
+      0x0000b571,
+      0x0000ffa0,
+      0x0065ffc9,
+      0x009cffdb,
+
+      0x00004747,
+      0x00007474,
+      0x0000b5b5,
+      0x0000ffff,
+      0x006dffff,
+      0x009cffff,
+
+      0x00002747,
+      0x00004074,
+      0x000063b5,
+      0x00008cff,
+      0x0059b4ff,
+      0x009cd3ff,
+
+      0x00000047,
+      0x00000074,
+      0x000000b5,
+      0x000000ff,
+      0x005959ff,
+      0x009c9cff,
+
+      0x002e0047,
+      0x004b0074,
+      0x007500b5,
+      0x00a500ff,
+      0x00c459ff,
+      0x00dc9cff,
+
+      0x00470047,
+      0x00740074,
+      0x00b500b5,
+      0x00ff00ff,
+      0x00ff66ff,
+      0x00ff9cff,
+
+      0x0047002a,
+      0x00740045,
+      0x00b5006b,
+      0x00ff0098,
+      0x00ff59bc,
+      0x00ff94d3,
+
+      0x00000000,
+      0x00131313,
+      0x00282828,
+      0x00363636,
+      0x004d4d4d,
+      0x00656565,
+      0x00818181,
+      0x009f9f9f,
+      0x00bcbcbc,
+      0x00e2e2e2,
+      0x00ffffff
+    )
+  }
+}
diff --git a/gradle.properties b/gradle.properties
index bf7e5319a..9450fb27e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -33,7 +33,7 @@ android.enableD8=true
 # Enable new Android R8 Optimizer
 android.enableR8=true
 # Enable gradle build cache
-org.gradle.caching=false
+org.gradle.caching=true
 # Enable android build cache
 android.enableBuildCache=true
 # Enable AndroidX
-- 
GitLab