From 9739c4bdef9975a1ef187fba274d2da547fca54c Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Mon, 2 Apr 2018 13:17:59 +0200
Subject: [PATCH] Implement significantly faster DayChange headers

---
 .../ui/chat/input/AutoCompleteAdapter.kt      |  14 ++-
 .../chat/messages/DayChangeItemDecoration.kt  | 102 ++++++++++++++++++
 .../ui/chat/messages/DisplayMessage.kt        |   7 +-
 .../ui/chat/messages/MessageAdapter.kt        |  16 +--
 .../ui/chat/messages/MessageListFragment.kt   |  15 ++-
 .../ui/chat/nicks/NickListAdapter.kt          |  17 +--
 .../layout/widget_chatmessage_daychange.xml   |   6 +-
 app/src/main/res/values/ids.xml               |   6 ++
 .../persistence/QuasselBacklogStorage.kt      |   5 +-
 .../persistence/QuasselDatabase.kt            | 102 ++----------------
 10 files changed, 169 insertions(+), 121 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DayChangeItemDecoration.kt
 create mode 100644 app/src/main/res/values/ids.xml

diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt
index 0be424b74..7f295e55e 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteAdapter.kt
@@ -112,11 +112,15 @@ class AutoCompleteAdapter(
         nick.text = SpanFormatter.format("%s%s", data.modes, data.displayNick ?: data.nick)
         realname.text = data.realname
 
-        GlideApp.with(itemView)
-          .load(data.avatarUrl)
-          .apply(RequestOptions.circleCropTransform())
-          .placeholder(data.fallbackDrawable)
-          .into(avatar)
+        if (data.avatarUrl != null) {
+          GlideApp.with(itemView)
+            .load(data.avatarUrl)
+            .apply(RequestOptions.circleCropTransform())
+            .placeholder(data.fallbackDrawable)
+            .into(avatar)
+        } else {
+          avatar.setImageDrawable(data.fallbackDrawable)
+        }
       }
     }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DayChangeItemDecoration.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DayChangeItemDecoration.kt
new file mode 100644
index 000000000..660579438
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DayChangeItemDecoration.kt
@@ -0,0 +1,102 @@
+package de.kuschku.quasseldroid.ui.chat.messages
+
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import de.kuschku.quasseldroid.R
+import org.threeten.bp.ZoneId
+import org.threeten.bp.format.DateTimeFormatter
+import org.threeten.bp.format.FormatStyle
+import org.threeten.bp.temporal.ChronoUnit
+
+class DayChangeItemDecoration(private val adapter: MessageAdapter) :
+  RecyclerView.ItemDecoration() {
+  private val dayChangeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
+  private val mBounds = Rect()
+
+  override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
+    c.save()
+    val left: Int
+    val right: Int
+    if (parent.clipToPadding) {
+      left = parent.paddingLeft
+      right = parent.width - parent.paddingRight
+      c.clipRect(left, parent.paddingTop, right, parent.height - parent.paddingBottom)
+    } else {
+      left = 0
+      right = parent.width
+    }
+
+    val childCount = parent.childCount
+    for (i in 0 until childCount) {
+      val child = parent.getChildAt(i)
+      if (child.getTag(R.id.tag_daychange) == true) {
+        parent.getDecoratedBoundsWithMargins(child, mBounds)
+        val bottom = mBounds.bottom + Math.round(child.translationY)
+        val top = mBounds.top + Math.round(child.translationY)
+        val layout = child.getTag(R.id.tag_daychange_layout) as View
+        c.save()
+        c.clipRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
+        c.translate(left.toFloat(), top.toFloat())
+        layout.draw(c)
+        c.restore()
+      }
+    }
+    c.restore()
+  }
+
+  private fun fixLayoutSize(view: View, parent: ViewGroup) {
+    val widthSpec = View.MeasureSpec.makeMeasureSpec(
+      parent.width,
+      View.MeasureSpec.EXACTLY
+    )
+    val heightSpec = View.MeasureSpec.makeMeasureSpec(
+      parent.height,
+      View.MeasureSpec.UNSPECIFIED
+    )
+
+    val childWidthSpec = ViewGroup.getChildMeasureSpec(
+      widthSpec,
+      parent.paddingLeft + parent.paddingRight,
+      view.layoutParams.width
+    )
+    val childHeightSpec = ViewGroup.getChildMeasureSpec(
+      heightSpec,
+      parent.paddingTop + parent.paddingBottom,
+      view.layoutParams.height
+    )
+
+    view.measure(childWidthSpec, childHeightSpec)
+    view.layout(0, 0, view.measuredWidth, view.measuredHeight)
+  }
+
+  override fun getItemOffsets(outRect: Rect, v: View, parent: RecyclerView,
+                              state: RecyclerView.State) {
+    adapter[parent.getChildAdapterPosition(v)]?.let {
+      if (it.hasDayChange) {
+        if (v.getTag(R.id.tag_daychange_layout) == null) {
+          val layout = LayoutInflater.from(parent.context).inflate(
+            R.layout.widget_chatmessage_daychange, parent, false
+          )
+          val content = layout.findViewById<TextView>(R.id.combined)
+          content?.text = dayChangeFormatter.format(
+            it.content.time.atZone(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS)
+          )
+          fixLayoutSize(layout, parent)
+
+          v.setTag(R.id.tag_daychange_layout, layout)
+          v.setTag(R.id.tag_daychange_content, content)
+        }
+        v.setTag(R.id.tag_daychange, true)
+        val layout = v.getTag(R.id.tag_daychange_layout) as View
+        outRect.set(0, layout.measuredHeight, 0, 10)
+      } else {
+        v.setTag(R.id.tag_daychange, false)
+      }
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt
index 8a3f6fb43..ab5633dfa 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/DisplayMessage.kt
@@ -6,22 +6,25 @@ import de.kuschku.quasseldroid.persistence.QuasselDatabase
 
 data class DisplayMessage(
   val content: QuasselDatabase.DatabaseMessage,
+  val hasDayChange: Boolean,
+  val isFollowUp: Boolean,
   val isSelected: Boolean,
   val isExpanded: Boolean,
   val isMarkerLine: Boolean
 ) {
   data class Tag(
     val id: MsgId,
+    val hasDayChange: Boolean,
     val isFollowUp: Boolean,
     val isSelected: Boolean,
     val isExpanded: Boolean,
     val isMarkerLine: Boolean
   )
 
-  val tag = Tag(content.messageId, content.followUp, isSelected, isExpanded, isMarkerLine)
+  val tag = Tag(content.messageId, hasDayChange, isFollowUp, isSelected, isExpanded, isMarkerLine)
   val avatarUrl = content.sender.let {
     Regex("[us]id(\\d+)").matchEntire(HostmaskHelper.user(it))?.groupValues?.lastOrNull()?.let {
       "https://www.irccloud.com/avatar-redirect/$it"
     }
   }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
index 191abda65..0cbe4e1db 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageAdapter.kt
@@ -61,7 +61,7 @@ class MessageAdapter(
   override fun getItemViewType(position: Int) = getItem(position)?.let {
     viewType(Message_Flags.of(it.content.type),
              Message_Flags.of(it.content.flag),
-             it.content.followUp)
+             it.isFollowUp)
   } ?: 0
 
   private fun viewType(type: Message_Types, flags: Message_Flags, followUp: Boolean) =
@@ -200,11 +200,15 @@ class MessageAdapter(
       this.itemView.isSelected = message.isSelected
 
       avatar?.let { avatarView ->
-        GlideApp.with(itemView)
-          .load(message.avatarUrl)
-          .apply(RequestOptions.circleCropTransform())
-          .placeholder(message.fallbackDrawable)
-          .into(avatarView)
+        if (message.avatarUrl != null) {
+          GlideApp.with(itemView)
+            .load(message.avatarUrl)
+            .apply(RequestOptions.circleCropTransform())
+            .placeholder(message.fallbackDrawable)
+            .into(avatarView)
+        } else {
+          avatarView.setImageDrawable(message.fallbackDrawable)
+        }
       }
     }
   }
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 8d7fa43c8..22d11cf69 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
@@ -26,7 +26,6 @@ import de.kuschku.libquassel.util.helpers.value
 import de.kuschku.quasseldroid.GlideApp
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.persistence.QuasselDatabase
-import de.kuschku.quasseldroid.persistence.findByBufferIdPagedWithDayChange
 import de.kuschku.quasseldroid.settings.AppearanceSettings
 import de.kuschku.quasseldroid.settings.BacklogSettings
 import de.kuschku.quasseldroid.settings.MessageSettings
@@ -34,6 +33,9 @@ import de.kuschku.quasseldroid.util.helper.*
 import de.kuschku.quasseldroid.util.service.ServiceBoundFragment
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import io.reactivex.BackpressureStrategy
+import org.threeten.bp.ZoneId
+import org.threeten.bp.ZonedDateTime
+import org.threeten.bp.temporal.ChronoUnit
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
@@ -234,11 +236,17 @@ class MessageListFragment : ServiceBoundFragment() {
     fun processMessages(list: List<QuasselDatabase.DatabaseMessage>, selected: Set<MsgId>,
                         expanded: Set<MsgId>, markerLine: MsgId?): List<DisplayMessage> {
       var previous: QuasselDatabase.DatabaseMessage? = null
+      var previousDate: ZonedDateTime? = null
       return list.asReversed().map {
-        it.followUp = previous?.sender == it.sender
+        val date = it.time.atZone(ZoneId.systemDefault()).truncatedTo(ChronoUnit.DAYS)
+        val isSameDay = previousDate?.isEqual(date) ?: false
+        val isFollowUp = previous?.sender == it.sender && isSameDay
         previous = it
+        previousDate = date
         DisplayMessage(
           content = it,
+          hasDayChange = !isSameDay,
+          isFollowUp = isFollowUp,
           isSelected = selected.contains(it.messageId),
           isExpanded = expanded.contains(it.messageId),
           isMarkerLine = markerLine == it.messageId
@@ -253,7 +261,7 @@ class MessageListFragment : ServiceBoundFragment() {
       .toLiveData().switchMapNotNull { (buffer, selected, expanded, markerLine) ->
         database.filtered().listen(accountId, buffer).switchMapNotNull { filtered ->
           LivePagedListBuilder(
-            database.message().findByBufferIdPagedWithDayChange(buffer, filtered).mapByPage {
+            database.message().findByBufferIdPaged(buffer, filtered).mapByPage {
               processMessages(it, selected.keys, expanded, markerLine.orNull())
             },
             PagedList.Config.Builder()
@@ -368,6 +376,7 @@ class MessageListFragment : ServiceBoundFragment() {
     val preloader = RecyclerViewPreloader(Glide.with(this), preloadModelProvider, sizeProvider, 10)
 
     messageList.addOnScrollListener(preloader)
+    messageList.addItemDecoration(DayChangeItemDecoration(adapter))
 
     return view
   }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt
index 4f07b965b..92d862977 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt
@@ -86,12 +86,15 @@ class NickListAdapter(
       nick.text = SpanFormatter.format("%s%s", data.modes, data.displayNick ?: data.nick)
       realname.text = data.realname
 
-
-      GlideApp.with(itemView)
-        .load(data.avatarUrl)
-        .apply(RequestOptions.circleCropTransform())
-        .placeholder(data.fallbackDrawable)
-        .into(avatar)
+      if (data.avatarUrl != null) {
+        GlideApp.with(itemView)
+          .load(data.avatarUrl)
+          .apply(RequestOptions.circleCropTransform())
+          .placeholder(data.fallbackDrawable)
+          .into(avatar)
+      } else {
+        avatar.setImageDrawable(data.fallbackDrawable)
+      }
     }
   }
 
@@ -99,4 +102,4 @@ class NickListAdapter(
     const val VIEWTYPE_ACTIVE = 0
     const val VIEWTYPE_AWAY = 1
   }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/layout/widget_chatmessage_daychange.xml b/app/src/main/res/layout/widget_chatmessage_daychange.xml
index fedd0454c..eda4795f9 100644
--- a/app/src/main/res/layout/widget_chatmessage_daychange.xml
+++ b/app/src/main/res/layout/widget_chatmessage_daychange.xml
@@ -7,6 +7,10 @@
   android:orientation="vertical"
   android:textAppearance="?android:attr/textAppearanceListItemSmall">
 
+  <Space
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/message_vertical" />
+
   <View
     android:layout_width="match_parent"
     android:layout_height="1dp"
@@ -33,4 +37,4 @@
       android:textStyle="bold"
       tools:text="27.03.2018" />
   </LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 000000000..07001df1e
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <item name="tag_daychange" type="id" />
+  <item name="tag_daychange_layout" type="id" />
+  <item name="tag_daychange_content" type="id" />
+</resources>
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
index 7ee165112..2c79f758b 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselBacklogStorage.kt
@@ -29,8 +29,7 @@ class QuasselBacklogStorage(private val db: QuasselDatabase) : BacklogStorage {
           bufferId = message.bufferInfo.bufferId,
           sender = message.sender,
           senderPrefixes = message.senderPrefixes,
-          content = message.content,
-          followUp = false
+          content = message.content
         )
       )
     }
@@ -48,4 +47,4 @@ class QuasselBacklogStorage(private val db: QuasselDatabase) : BacklogStorage {
     db.message().clearMessages()
   }
 
-}
\ No newline at end of file
+}
diff --git a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
index a72a63436..857f984fa 100644
--- a/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
+++ b/persistence/src/main/java/de/kuschku/quasseldroid/persistence/QuasselDatabase.kt
@@ -2,7 +2,6 @@ package de.kuschku.quasseldroid.persistence
 
 import android.arch.lifecycle.LiveData
 import android.arch.paging.DataSource
-import android.arch.persistence.db.SimpleSQLiteQuery
 import android.arch.persistence.db.SupportSQLiteDatabase
 import android.arch.persistence.db.SupportSQLiteQuery
 import android.arch.persistence.room.*
@@ -17,7 +16,7 @@ import de.kuschku.quasseldroid.persistence.QuasselDatabase.Filtered
 import io.reactivex.Flowable
 import org.threeten.bp.Instant
 
-@Database(entities = [DatabaseMessage::class, Filtered::class], version = 4)
+@Database(entities = [DatabaseMessage::class, Filtered::class], version = 5)
 @TypeConverters(DatabaseMessage.MessageTypeConverters::class)
 abstract class QuasselDatabase : RoomDatabase() {
   abstract fun message(): MessageDao
@@ -32,8 +31,7 @@ abstract class QuasselDatabase : RoomDatabase() {
     var bufferId: Int,
     var sender: String,
     var senderPrefixes: String,
-    var content: String,
-    var followUp: Boolean
+    var content: String
   ) {
     class MessageTypeConverters {
       @TypeConverter
@@ -177,6 +175,12 @@ abstract class QuasselDatabase : RoomDatabase() {
                     "ALTER TABLE message ADD followUp INT DEFAULT 0 NOT NULL;"
                   )
                 }
+              },
+              object : Migration(4, 5) {
+                override fun migrate(database: SupportSQLiteDatabase) {
+                  database.execSQL("drop table message;")
+                  database.execSQL("create table message (messageId INTEGER not null primary key, time INTEGER not null, type INTEGER not null, flag INTEGER not null, bufferId INTEGER not null, sender TEXT not null, senderPrefixes TEXT not null, content TEXT not null);")
+                }
               }
             ).build()
           }
@@ -197,93 +201,3 @@ fun QuasselDatabase.MessageDao.clearMessages(
 ) {
   this.clearMessages(bufferId, idRange.first, idRange.last)
 }
-
-fun QuasselDatabase.MessageDao.findByBufferIdPagedWithDayChange(bufferId: Int, type: Int) =
-  this.findMessagesRawPaged(SimpleSQLiteQuery("""
-SELECT t.*
-FROM
-  (
-    SELECT
-      messageId,
-      time,
-      type,
-      flag,
-      bufferId,
-      sender,
-      senderPrefixes,
-      content,
-      followUp
-    FROM message
-    WHERE bufferId = ?
-          AND type & ~? > 0
-    UNION ALL
-    SELECT DISTINCT
-      strftime('%s', date(datetime(time / 1000, 'unixepoch', 'localtime')), 'utc') * -1000 AS messageId,
-      strftime('%s', date(datetime(time / 1000, 'unixepoch', 'localtime')), 'utc') * 1000  AS time,
-      8192                                                                                 AS type,
-      0                                                                                    AS flag,
-      ?                                                                                    AS bufferId,
-      ''                                                                                   AS sender,
-      ''                                                                                   AS senderPrefixes,
-      ''                                                                                   AS content,
-      0                                                                                    AS followUp
-    FROM message
-    WHERE bufferId = ?
-          AND type & ~? > 0
-  ) t
-ORDER BY TIME
-  DESC, messageId
-  DESC
-  """, arrayOf(bufferId, type, bufferId, bufferId, type)))
-
-fun QuasselDatabase.MessageDao.findByBufferIdPagedWithDayChangeSlow(bufferId: Int, type: Int) =
-  this.findMessagesRawPaged(SimpleSQLiteQuery("""
-SELECT t.*
-FROM
-  (
-    SELECT
-      messageId,
-      time,
-      type,
-      flag,
-      bufferId,
-      sender,
-      senderPrefixes,
-      content,
-      (SELECT 1
-       FROM
-         (SELECT *
-          FROM message m
-          WHERE m.messageId < message.messageId
-                AND bufferId = ?
-                AND type & ~? > 0
-          ORDER BY m.messageId
-            DESC
-          LIMIT 1) t
-       WHERE t.sender = message.sender
-             AND strftime('%s', date(datetime(t.time / 1000, 'unixepoch', 'localtime')), 'utc') * 1000 =
-                 strftime('%s', date(datetime(message.time / 1000, 'unixepoch', 'localtime')), 'utc') * 1000
-             AND t.type = message.type
-      ) = 1 AS followUp
-    FROM message
-    WHERE bufferId = ?
-          AND type & ~? > 0
-    UNION ALL
-    SELECT DISTINCT
-      strftime('%s', date(datetime(time / 1000, 'unixepoch', 'localtime')), 'utc') * -1000 AS messageId,
-      strftime('%s', date(datetime(time / 1000, 'unixepoch', 'localtime')), 'utc') * 1000  AS time,
-      8192                                                                                 AS type,
-      0                                                                                    AS flag,
-      ?                                                                                    AS bufferId,
-      ''                                                                                   AS sender,
-      ''                                                                                   AS senderPrefixes,
-      ''                                                                                   AS content,
-      0                                                                                    AS followUp
-    FROM message
-    WHERE bufferId = ?
-          AND type & ~? > 0
-  ) t
-ORDER BY TIME
-  DESC, messageId
-  DESC
-  """, arrayOf(bufferId, type, bufferId, type, bufferId, bufferId, type)))
-- 
GitLab