Refactor search

This commit is contained in:
MM20
2024-07-17 23:27:00 +02:00
parent 5a28eb584e
commit c7804296d7
6 changed files with 351 additions and 207 deletions
@@ -1,7 +1,6 @@
package de.mm20.launcher2.ui.launcher.search
import android.content.Context
import android.os.Process
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
@@ -23,11 +22,11 @@ import de.mm20.launcher2.search.Article
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.Contact
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.search.Website
import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.search.data.UnitConverter
@@ -36,17 +35,14 @@ import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -78,6 +74,7 @@ class SearchVM : ViewModel(), KoinComponent {
val locationResults = mutableStateOf<List<Location>>(emptyList())
val appResults = mutableStateOf<List<Application>>(emptyList())
val workAppResults = mutableStateOf<List<Application>>(emptyList())
val privateSpaceAppResults = mutableStateOf<List<Application>>(emptyList())
val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList())
val fileResults = mutableStateOf<List<File>>(emptyList())
val contactResults = mutableStateOf<List<Contact>>(emptyList())
@@ -96,7 +93,11 @@ class SearchVM : ViewModel(), KoinComponent {
val showFilters = mutableStateOf(false)
private val defaultFilters = searchFilterSettings.defaultFilter.stateIn(viewModelScope, SharingStarted.Eagerly, SearchFilters())
private val defaultFilters = searchFilterSettings.defaultFilter.stateIn(
viewModelScope,
SharingStarted.Eagerly,
SearchFilters()
)
val filters = mutableStateOf(defaultFilters.value)
val filterBar = searchFilterSettings.filterBar
val filterBarItems = searchFilterSettings.filterBarItems
@@ -174,146 +175,191 @@ class SearchVM : ViewModel(), KoinComponent {
} catch (_: CancellationException) {
}
hideFavorites.value = query.isNotEmpty()
searchJob = viewModelScope.launch {
searchUiSettings.resultOrder.collectLatest { resultOrder ->
searchService.search(
query,
filters = if (query.isEmpty()) filters.copy(apps = true) else filters,
).collectLatest { results ->
var resultsList = withContext(Dispatchers.Default) {
listOfNotNull(
results.apps,
results.other,
results.shortcuts,
results.files,
results.contacts,
results.calendars,
results.locations,
results.wikipedia,
results.websites,
results.calculators,
results.unitConverters,
results.searchActions,
).flatten()
.distinctBy { if (it is SavableSearchable) it.key else it }
.sortedBy { (it as? SavableSearchable) }
}
if (query.isEmpty()) {
val hiddenItemKeys = searchableRepository.getKeys(
maxVisibility = VisibilityLevel.SearchOnly,
includeTypes = listOf("app"),
)
val allApps = searchService.getAllApps()
fileResults.value = emptyList()
contactResults.value = emptyList()
calendarResults.value = emptyList()
locationResults.value = emptyList()
articleResults.value = emptyList()
websiteResults.value = emptyList()
calculatorResults.value = emptyList()
unitConverterResults.value = emptyList()
searchActionResults.value = emptyList()
val relevance =
if (query.isEmpty()) {
emptyList()
} else {
val keys = resultsList.mapNotNull { (it as? SavableSearchable)?.key }
when (resultOrder) {
allApps
.combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys }
.collectLatest { (results, hiddenKeys) ->
val hiddenItems = mutableListOf<SavableSearchable>()
SearchResultOrder.LaunchCount -> searchableRepository.sortByRelevance(
keys
).first()
SearchResultOrder.Weighted -> searchableRepository.sortByWeight(
keys
).first()
else -> emptyList()
}
val (hiddenApps, apps) = results.standardProfileApps.partition {
hiddenKeys.contains(
it.key
)
}
hiddenItems += hiddenApps
resultsList = resultsList.sortedWith { a, b ->
val lastLocation = devicePoseProvider.lastLocation
when {
a is Location && b is Location && lastLocation != null -> {
a.distanceTo(lastLocation)
.compareTo(b.distanceTo(lastLocation))
}
a is SavableSearchable && b !is SavableSearchable -> -1
a !is SavableSearchable && b is SavableSearchable -> 1
a is SavableSearchable && b is SavableSearchable -> {
val aKey = a.key
val bKey = b.key
val aRank = relevance.indexOf(aKey)
val bRank = relevance.indexOf(bKey)
when {
aRank != -1 && bRank != -1 -> aRank.compareTo(bRank)
aRank == -1 && bRank != -1 -> 1
aRank != -1 && bRank == -1 -> -1
else -> a.compareTo(b)
}
}
else -> 0
val (hiddenWorkApps, workApps) = results.workProfileApps.partition {
hiddenKeys.contains(
it.key
)
}
}
hiddenItems += hiddenWorkApps
val hiddenItemKeys = searchableRepository.getKeys(
maxVisibility = if (query.isEmpty()) VisibilityLevel.SearchOnly else VisibilityLevel.Hidden,
)
hiddenItemKeys.collectLatest { hiddenKeys ->
val hidden = mutableListOf<SavableSearchable>()
val apps = mutableListOf<Application>()
val workApps = mutableListOf<Application>()
val shortcuts = mutableListOf<AppShortcut>()
val files = mutableListOf<File>()
val contacts = mutableListOf<Contact>()
val events = mutableListOf<CalendarEvent>()
val unitConv = mutableListOf<UnitConverter>()
val calc = mutableListOf<Calculator>()
val articles = mutableListOf<Article>()
val locations = mutableListOf<Location>()
val website = mutableListOf<Website>()
val actions = mutableListOf<SearchAction>()
for (r in resultsList) {
when {
r is SavableSearchable && hiddenKeys.contains(r.key) && !filters.hiddenItems -> {
hidden.add(r)
}
r is Application && r.user != Process.myUserHandle() -> workApps.add(r)
r is Application -> apps.add(r)
r is AppShortcut -> shortcuts.add(r)
r is File -> files.add(r)
r is Contact -> contacts.add(r)
r is CalendarEvent -> events.add(r)
r is UnitConverter -> unitConv.add(r)
r is Calculator -> calc.add(r)
r is Website -> website.add(r)
r is Article -> articles.add(r)
r is Location -> locations.add(r)
r is SearchAction -> actions.add(r)
}
}
if (query.isNotEmpty() && launchOnEnter.value) {
bestMatch.value = listOf(
apps,
workApps,
shortcuts,
unitConv,
calc,
events,
locations,
contacts,
articles,
website,
files,
actions
).firstNotNullOfOrNull { it.firstOrNull() }
val (hiddenPrivateApps, privateApps) = results.privateSpaceApps.partition {
hiddenKeys.contains(
it.key
)
}
hiddenItems += hiddenPrivateApps
appResults.value = apps
workAppResults.value = workApps
appShortcutResults.value = shortcuts
fileResults.value = files
contactResults.value = contacts
calendarResults.value = events
articleResults.value = articles
locationResults.value = locations
websiteResults.value = website
calculatorResults.value = calc
unitConverterResults.value = unitConv
hiddenResults.value = hidden
if (results.searchActions != null) searchActionResults.value = actions
privateSpaceAppResults.value = privateApps
hiddenResults.value = hiddenItems
}
} else {
val hiddenItemKeys = searchableRepository.getKeys(
maxVisibility = VisibilityLevel.Hidden,
)
searchUiSettings.resultOrder.collectLatest { resultOrder ->
searchService.search(
query,
filters = if (query.isEmpty()) filters.copy(apps = true) else filters,
)
.combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys }
.collectLatest { (results, hiddenKeys) ->
val hiddenItems = mutableListOf<SavableSearchable>()
if (results.apps != null) {
val (hiddenApps, apps) = results.apps!!.partition {
hiddenKeys.contains(
it.key
)
}
hiddenItems += hiddenApps
appResults.value = apps.applyRanking(resultOrder)
} else {
appResults.value = emptyList()
}
workAppResults.value = emptyList()
privateSpaceAppResults.value = emptyList()
if (results.shortcuts != null) {
val (hiddenShortcuts, shortcuts) = results.shortcuts!!.partition {
hiddenKeys.contains(
it.key
)
}
hiddenItems += hiddenShortcuts
appShortcutResults.value = shortcuts.applyRanking(resultOrder)
} else {
appShortcutResults.value = emptyList()
}
if (results.files != null) {
val (hiddenFiles, files) = results.files!!.partition {
hiddenKeys.contains(
it.key
)
}
hiddenItems += hiddenFiles
fileResults.value = files.applyRanking(resultOrder)
} else {
fileResults.value = emptyList()
}
if (results.contacts != null) {
val (hiddenContacts, contacts) = results.contacts!!.partition {
hiddenKeys.contains(
it.key
)
}
hiddenItems += hiddenContacts
contactResults.value = contacts.applyRanking(resultOrder)
} else {
contactResults.value = emptyList()
}
if (results.calendars != null) {
val (hiddenEvents, events) = results.calendars!!.partition {
hiddenKeys.contains(
it.key
)
}
hiddenItems += hiddenEvents
calendarResults.value = events.applyRanking(resultOrder)
} else {
calendarResults.value = emptyList()
}
if (results.locations != null && results.locations!!.isNotEmpty()) {
val (hiddenLocations, locations) = results.locations!!.partition {
hiddenKeys.contains(
it.key
)
}
hiddenItems += hiddenLocations
val lastLocation = devicePoseProvider.lastLocation
if (lastLocation != null) {
locationResults.value = locations.asSequence()
.sortedWith { a, b ->
a.distanceTo(lastLocation)
.compareTo(b.distanceTo(lastLocation))
}
.distinct()
.toList()
} else {
locationResults.value = locations.applyRanking(resultOrder)
}
} else {
locationResults.value = emptyList()
}
if (results.wikipedia != null) {
articleResults.value = results.wikipedia!!.applyRanking(resultOrder)
} else {
articleResults.value = emptyList()
}
if (results.websites != null) {
websiteResults.value = results.websites!!.applyRanking(resultOrder)
} else {
websiteResults.value = emptyList()
}
calculatorResults.value = results.calculators ?: emptyList()
unitConverterResults.value = results.unitConverters ?: emptyList()
if (results.searchActions != null) {
searchActionResults.value = results.searchActions!!
}
if (launchOnEnter.value) {
bestMatch.value = when {
appResults.value.isNotEmpty() -> appResults.value.first()
appShortcutResults.value.isNotEmpty() -> appShortcutResults.value.first()
calendarResults.value.isNotEmpty() -> calendarResults.value.first()
locationResults.value.isNotEmpty() -> locationResults.value.first()
contactResults.value.isNotEmpty() -> contactResults.value.first()
articleResults.value.isNotEmpty() -> articleResults.value.first()
websiteResults.value.isNotEmpty() -> websiteResults.value.first()
fileResults.value.isNotEmpty() -> fileResults.value.first()
searchActionResults.value.isNotEmpty() -> searchActionResults.value.first()
else -> null
}
} else {
bestMatch.value = null
}
}
}
}
}
@@ -387,8 +433,30 @@ class SearchVM : ViewModel(), KoinComponent {
fun expandCategory(category: SearchCategory) {
expandedCategory.value = category
}
private suspend fun <T : SavableSearchable> List<T>.applyRanking(order: SearchResultOrder): List<T> {
if (size <= 1) return this
val sequence = asSequence()
val sorted = if (order == SearchResultOrder.Weighted) {
val sortedKeys = searchableRepository.sortByWeight(map { it.key }).first()
sequence.sortedWith { a, b ->
val aRank = sortedKeys.indexOf(a.key)
val bRank = sortedKeys.indexOf(b.key)
when {
aRank != -1 && bRank != -1 -> aRank.compareTo(bRank)
aRank == -1 && bRank != -1 -> 1
aRank != -1 && bRank == -1 -> -1
else -> a.compareTo(b)
}
}
} else {
sequence.sorted()
}
return sorted.distinct().toList()
}
}
enum class SearchCategory {
Apps,
Calculator,