diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index 67f758a4..d96e6c11 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -9,7 +9,6 @@ import de.mm20.launcher2.devicepose.DevicePoseProvider import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.preferences.SearchResultOrder import de.mm20.launcher2.preferences.search.CalendarSearchSettings import de.mm20.launcher2.preferences.search.ContactSearchSettings import de.mm20.launcher2.preferences.search.FileSearchSettings @@ -26,6 +25,7 @@ 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.ResultScore import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchFilters import de.mm20.launcher2.search.SearchService @@ -33,6 +33,7 @@ import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Website import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.UnitConverter +import de.mm20.launcher2.search.isUnspecified import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.searchable.VisibilityLevel import de.mm20.launcher2.searchactions.actions.SearchAction @@ -46,7 +47,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -264,138 +264,136 @@ class SearchVM : ViewModel(), KoinComponent { val hiddenItemKeys = if (!filters.hiddenItems) searchableRepository.getKeys( maxVisibility = VisibilityLevel.Hidden, ) else flowOf(emptyList()) - 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() + 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() - 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)) - } - .distinctBy { it.key } - .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 + if (results.apps != null) { + val (hiddenApps, apps) = results.apps!!.partition { + hiddenKeys.contains( + it.key + ) } + hiddenItems += hiddenApps + appResults.value = apps.applyRanking(query) + } 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(query) + } else { + appShortcutResults.value = emptyList() + } + + if (results.files != null) { + val (hiddenFiles, files) = results.files!!.partition { + hiddenKeys.contains( + it.key + ) + } + hiddenItems += hiddenFiles + fileResults.value = files.applyRanking(query) + } else { + fileResults.value = emptyList() + } + + if (results.contacts != null) { + val (hiddenContacts, contacts) = results.contacts!!.partition { + hiddenKeys.contains( + it.key + ) + } + hiddenItems += hiddenContacts + contactResults.value = contacts.applyRanking(query) + } else { + contactResults.value = emptyList() + } + + if (results.calendars != null) { + val (hiddenEvents, events) = results.calendars!!.partition { + hiddenKeys.contains( + it.key + ) + } + hiddenItems += hiddenEvents + calendarResults.value = events.applyRanking(query) + } 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)) + } + .distinctBy { it.key } + .toList() + } else { + locationResults.value = locations.applyRanking(query) + } + } else { + locationResults.value = emptyList() + } + + if (results.wikipedia != null) { + articleResults.value = results.wikipedia!!.applyRanking(query) + } else { + articleResults.value = emptyList() + } + + if (results.websites != null) { + websiteResults.value = results.websites!!.applyRanking(query) + } 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 + } + } } } } @@ -469,7 +467,7 @@ class SearchVM : ViewModel(), KoinComponent { expandedCategory.value = category } - private suspend fun List.applyRanking(order: SearchResultOrder): List { + private suspend fun List.applyRanking(query: String): List { if (size <= 1) return this val sequence = asSequence() val weights = searchableRepository.getWeights(map { it.key }).first() @@ -477,10 +475,22 @@ class SearchVM : ViewModel(), KoinComponent { val aWeight = weights[a.key] ?: 0.0 val bWeight = weights[b.key] ?: 0.0 - val aScore = a.score.score * 0.7f + aWeight.toFloat() * 0.3f - val bScore = b.score.score * 0.7f + bWeight.toFloat() * 0.3f + val aScore = if (a.score.isUnspecified) { + ResultScore(query = query, primaryFields = listOf(a.labelOverride ?: a.label)).score + } else { + a.score.score + } - bScore.compareTo(aScore) + val bScore = if (b.score.isUnspecified) { + ResultScore(query = query, primaryFields = listOf(b.labelOverride ?: b.label)).score + } else { + b.score.score + } + + val aTotal = aScore * 0.7f + aWeight.toFloat() * 0.3f + val bTotal = bScore * 0.7f + bWeight.toFloat() * 0.3f + + bTotal.compareTo(aTotal) } return sorted.distinctBy { it.key }.toList() } diff --git a/core/base/src/main/java/de/mm20/launcher2/search/ResultScore.kt b/core/base/src/main/java/de/mm20/launcher2/search/ResultScore.kt index 6e3d868c..90b6135f 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/ResultScore.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/ResultScore.kt @@ -45,7 +45,7 @@ value class ResultScore private constructor(private val packed: Long) : Comparab * A total score for the result, combining the similarity with additional factors. */ val score: Float - get() = (similarity + (if (isPrefix) 0.8f else 0f) + (if (isSubstring) 0.8f else 0f)) * (if (isPrimary) 1f else 0.5f) + get() = (similarity + (if (isPrefix) 0.8f else 0f) + (if (isSubstring) 0.8f else 0f)) * (if (isPrimary) 1f else 0.8f) override fun compareTo(other: ResultScore): Int { return score.compareTo(other.score) @@ -89,6 +89,15 @@ value class ResultScore private constructor(private val packed: Long) : Comparab isPrimary = false, similarity = 0f ) - } -} \ No newline at end of file + val Unspecified = ResultScore( + isPrefix = false, + isSubstring = false, + isPrimary = false, + similarity = Float.NaN, + ) + } +} + +inline val ResultScore.isUnspecified : Boolean + get() = this == ResultScore.Unspecified \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Searchable.kt b/core/base/src/main/java/de/mm20/launcher2/search/Searchable.kt index bc3c21b1..d359192c 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/Searchable.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/Searchable.kt @@ -4,5 +4,5 @@ import kotlinx.coroutines.Deferred interface Searchable { val score: ResultScore - get() = ResultScore.Zero + get() = ResultScore.Unspecified } \ No newline at end of file diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt index 3db58c90..b30738e6 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt @@ -264,14 +264,6 @@ internal class AppRepositoryImpl( } } - private fun matches(label: String, query: String): Boolean { - val normalizedLabel = label.normalize() - val normalizedQuery = query.normalize() - if (normalizedLabel.contains(normalizedQuery)) return true - val fuzzyScore = FuzzyScore(Locale.getDefault()) - return fuzzyScore.fuzzyScore(normalizedLabel, normalizedQuery) >= query.length * 1.5 - } - private fun getActivityByComponentName(componentName: ComponentName?): LauncherApp? { componentName ?: return null val intent = Intent().setComponent(componentName) diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt index 0ba39f23..7eb6760c 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt @@ -41,7 +41,7 @@ internal data class LauncherApp( override val isSuspended: Boolean = false, internal val userSerialNumber: Long, override val labelOverride: String? = null, - override val score: ResultScore = ResultScore.Zero, + override val score: ResultScore = ResultScore.Unspecified, ) : Application { override val componentName: ComponentName @@ -50,7 +50,7 @@ internal data class LauncherApp( override val label: String = launcherActivityInfo.label.toString() - constructor(context: Context, launcherActivityInfo: LauncherActivityInfo, score: ResultScore = ResultScore.Zero) : this( + constructor(context: Context, launcherActivityInfo: LauncherActivityInfo, score: ResultScore = ResultScore.Unspecified) : this( launcherActivityInfo, versionName = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName), isSuspended = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SUSPENDED != 0, diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt index d26a4b0d..4dee8303 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -14,6 +14,7 @@ import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.search.ShortcutSearchSettings import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.ResultScore import de.mm20.launcher2.search.SearchableRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -141,22 +142,20 @@ internal class AppShortcutRepositoryImpl( LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER ) val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle()) - ?.filter { - if (it.longLabel != null) { - return@filter matches(it.longLabel.toString(), query) - } - if (it.shortLabel != null) { - return@filter matches(it.shortLabel.toString(), query) - } - return@filter false + ?.mapNotNull { + val score = ResultScore( + query = query, + primaryFields = listOfNotNull(it.longLabel?.toString(), it.shortLabel?.toString()) + ) + if (score.score < 0.8f) return@mapNotNull null + LauncherShortcut( + context, + it, + score + ) } ?: emptyList() - shortcuts.mapNotNull { - LauncherShortcut( - context, - it - ) - }.toImmutableList() + shortcuts.toImmutableList() } else { persistentListOf() diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt index cf3c752b..f4bb3e68 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt @@ -19,6 +19,7 @@ import de.mm20.launcher2.icons.* import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.ResultScore import de.mm20.launcher2.search.SearchableSerializer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -32,6 +33,7 @@ internal data class LauncherShortcut( override val appName: String?, internal val userSerialNumber: Long, override val labelOverride: String? = null, + override val score: ResultScore = ResultScore.Unspecified, ) : AppShortcut { override val domain: String = Domain @@ -47,6 +49,7 @@ internal data class LauncherShortcut( constructor( context: Context, launcherShortcut: ShortcutInfo, + score: ResultScore = ResultScore.Unspecified, ): this( launcherShortcut = launcherShortcut, appName = try { @@ -55,7 +58,8 @@ internal data class LauncherShortcut( } catch (e: PackageManager.NameNotFoundException) { null }, - userSerialNumber = launcherShortcut.userHandle.getSerialNumber(context) + userSerialNumber = launcherShortcut.userHandle.getSerialNumber(context), + score = score, ) override val label: String diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt index 6f972df8..fbc34f02 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt @@ -87,7 +87,7 @@ class PluginFileProvider( uri = cursor[FileColumns.ContentUri]?.let { Uri.parse(it) } ?: continue, thumbnailUri = cursor[FileColumns.ThumbnailUri]?.let { Uri.parse(it) }, storageStrategy = config.storageStrategy, - isDirectory = cursor[FileColumns.IsDirectory] ?: false, + isDirectory = cursor[FileColumns.IsDirectory] == true, authority = pluginAuthority, timestamp = timestamp, updatedSelf = {