Message fuzzy search (#869)

This commit is contained in:
Dominik Korsa 2020-06-14 14:05:24 +02:00 committed by GitHub
parent 924bcb0d64
commit 6e1ddb482e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 130 additions and 78 deletions

View File

@ -142,7 +142,7 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.1.0"
implementation "com.github.wulkanowy:material-chips-input:2.0.1"
implementation "com.github.wulkanowy:material-chips-input:2.1.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
@ -180,6 +180,7 @@ dependencies {
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
implementation "io.coil-kt:coil:0.11.0"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
playImplementation 'com.google.firebase:firebase-analytics:17.4.3'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.7'

View File

@ -4,10 +4,9 @@ import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.message.MessageFolder
@ -20,39 +19,23 @@ class MessageTabAdapter @Inject constructor() :
var onClickListener: (Message, position: Int) -> Unit = { _, _ -> }
private val items = SortedList(Message::class.java, object :
SortedListAdapterCallback<Message>(this) {
private var items = mutableListOf<Message>()
override fun compare(item1: Message, item2: Message): Int {
return item2.date.compareTo(item1.date)
}
override fun areContentsTheSame(oldItem: Message?, newItem: Message?): Boolean {
return oldItem == newItem
}
override fun areItemsTheSame(item1: Message, item2: Message): Boolean {
return item1 == item2
}
})
fun replaceAll(models: List<Message>) {
items.beginBatchedUpdates()
for (i in items.size() - 1 downTo 0) {
val model = items.get(i)
if (model !in models) {
items.remove(model)
}
}
items.addAll(models)
items.endBatchedUpdates()
fun setDataItems(data: List<Message>) {
val diffResult = DiffUtil.calculateDiff(MessageTabDiffUtil(items, data))
items = data.toMutableList()
diffResult.dispatchUpdatesTo(this)
}
fun updateItem(position: Int, item: Message) {
items.updateItemAt(position, item)
val currentItem = items[position]
items[position] = item
if (item != currentItem) {
notifyItemChanged(position)
}
}
override fun getItemCount() = items.size()
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
ItemMessageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@ -85,4 +68,19 @@ class MessageTabAdapter @Inject constructor() :
}
class ItemViewHolder(val binding: ItemMessageBinding) : RecyclerView.ViewHolder(binding.root)
private class MessageTabDiffUtil(private val old: List<Message>, private val new: List<Message>) :
DiffUtil.Callback() {
override fun getOldListSize(): Int = old.size
override fun getNewListSize(): Int = new.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition].id == new[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return old[oldItemPosition] == new[newItemPosition]
}
}
}

View File

@ -90,7 +90,7 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
}
override fun updateData(data: List<Message>) {
tabAdapter.replaceAll(data)
tabAdapter.setDataItems(data)
}
override fun updateItem(item: Message, position: Int) {

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.message.tab
import android.annotation.SuppressLint
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.message.MessageFolder
import io.github.wulkanowy.data.repositories.message.MessageRepository
@ -11,8 +10,13 @@ import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.toFormattedString
import io.reactivex.subjects.PublishSubject
import me.xdrop.fuzzywuzzy.FuzzySearch
import timber.log.Timber
import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.pow
class MessageTabPresenter @Inject constructor(
schedulers: SchedulersProvider,
@ -31,9 +35,12 @@ class MessageTabPresenter @Inject constructor(
private var messages = emptyList<Message>()
private val searchQuery = PublishSubject.create<String>()
fun onAttachView(view: MessageTabView, folder: MessageFolder) {
super.onAttachView(view)
view.initView()
initializeSearchStream()
errorHandler.showErrorMessage = ::showErrorViewOnError
this.folder = folder
}
@ -76,9 +83,7 @@ class MessageTabPresenter @Inject constructor(
private fun loadData(forceRefresh: Boolean) {
Timber.i("Loading $folder message data started")
disposable.apply {
clear()
add(studentRepository.getCurrentStudent()
disposable.add(studentRepository.getCurrentStudent()
.flatMap { student ->
semesterRepository.getCurrentSemester(student)
.flatMap { messageRepository.getMessages(student, it, folder, forceRefresh) }
@ -96,7 +101,7 @@ class MessageTabPresenter @Inject constructor(
.subscribe({
Timber.i("Loading $folder message result: Success")
messages = it
onSearchQueryTextChange(lastSearchQuery)
view?.updateData(getFilteredData(lastSearchQuery))
analytics.logEvent(
"load_data",
"type" to "messages",
@ -108,7 +113,6 @@ class MessageTabPresenter @Inject constructor(
errorHandler.dispatch(it)
})
}
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
@ -121,25 +125,36 @@ class MessageTabPresenter @Inject constructor(
}
}
@SuppressLint("DefaultLocale")
fun onSearchQueryTextChange(query: String) {
if (query != searchQuery.toString())
searchQuery.onNext(query)
}
private fun initializeSearchStream() {
disposable.add(searchQuery
.debounce(250, TimeUnit.MILLISECONDS)
.map { query ->
lastSearchQuery = query
val lowerCaseQuery = query.toLowerCase()
val filteredList = mutableListOf<Message>()
messages.forEach {
if (lowerCaseQuery in it.subject.toLowerCase() ||
lowerCaseQuery in it.sender.toLowerCase() ||
lowerCaseQuery in it.recipient.toLowerCase() ||
lowerCaseQuery in it.date.toFormattedString()
) {
filteredList.add(it)
getFilteredData(query)
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${it.size}")
updateData(it)
}) { Timber.e(it) })
}
Timber.d("Applying filter. Full list: ${messages.size}, filtered: ${filteredList.size}")
updateData(filteredList)
private fun getFilteredData(query: String): List<Message> {
return if (query.trim().isEmpty()) {
messages.sortedByDescending { it.date }
} else {
messages
.map { it to calculateMatchRatio(it, query) }
.sortedByDescending { it.second }
.filter { it.second > 5000 }
.map { it.first }
}
}
private fun updateData(data: List<Message>) {
@ -151,4 +166,42 @@ class MessageTabPresenter @Inject constructor(
resetListPosition()
}
}
private fun calculateMatchRatio(message: Message, query: String): Int {
val subjectRatio = FuzzySearch.tokenSortPartialRatio(
query.toLowerCase(Locale.getDefault()),
message.subject
)
val senderOrRecipientRatio = FuzzySearch.tokenSortPartialRatio(
query.toLowerCase(Locale.getDefault()),
if (message.sender.isNotEmpty()) message.sender.toLowerCase(Locale.getDefault())
else message.recipient.toLowerCase(Locale.getDefault())
)
val dateRatio = listOf(
FuzzySearch.ratio(
query.toLowerCase(Locale.getDefault()),
message.date.toFormattedString("dd.MM").toLowerCase(Locale.getDefault())
),
FuzzySearch.ratio(
query.toLowerCase(Locale.getDefault()),
message.date.toFormattedString("dd.MM.yyyy").toLowerCase(Locale.getDefault())
),
FuzzySearch.ratio(
query.toLowerCase(Locale.getDefault()),
message.date.toFormattedString("d MMMM").toLowerCase(Locale.getDefault())
),
FuzzySearch.ratio(
query.toLowerCase(Locale.getDefault()),
message.date.toFormattedString("d MMMM yyyy").toLowerCase(Locale.getDefault())
)
).max() ?: 0
return (subjectRatio.toDouble().pow(2)
+ senderOrRecipientRatio.toDouble().pow(2)
+ dateRatio.toDouble().pow(2) * 2
).toInt()
}
}