Change search result order for all other categories

This commit is contained in:
MM20 2024-10-13 20:32:15 +02:00
parent bf6e963545
commit 774777f79c
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 179 additions and 165 deletions

View File

@ -9,7 +9,6 @@ import de.mm20.launcher2.devicepose.DevicePoseProvider
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager 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.CalendarSearchSettings
import de.mm20.launcher2.preferences.search.ContactSearchSettings import de.mm20.launcher2.preferences.search.ContactSearchSettings
import de.mm20.launcher2.preferences.search.FileSearchSettings 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.Contact
import de.mm20.launcher2.search.File import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.Location import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.ResultScore
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchFilters import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.search.SearchService 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.Website
import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.search.data.UnitConverter import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.search.isUnspecified
import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.searchable.VisibilityLevel import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.searchactions.actions.SearchAction 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.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -264,7 +264,6 @@ class SearchVM : ViewModel(), KoinComponent {
val hiddenItemKeys = if (!filters.hiddenItems) searchableRepository.getKeys( val hiddenItemKeys = if (!filters.hiddenItems) searchableRepository.getKeys(
maxVisibility = VisibilityLevel.Hidden, maxVisibility = VisibilityLevel.Hidden,
) else flowOf(emptyList()) ) else flowOf(emptyList())
searchUiSettings.resultOrder.collectLatest { resultOrder ->
searchService.search( searchService.search(
query, query,
filters = if (query.isEmpty()) filters.copy(apps = true) else filters, filters = if (query.isEmpty()) filters.copy(apps = true) else filters,
@ -280,7 +279,7 @@ class SearchVM : ViewModel(), KoinComponent {
) )
} }
hiddenItems += hiddenApps hiddenItems += hiddenApps
appResults.value = apps.applyRanking(resultOrder) appResults.value = apps.applyRanking(query)
} else { } else {
appResults.value = emptyList() appResults.value = emptyList()
} }
@ -294,7 +293,7 @@ class SearchVM : ViewModel(), KoinComponent {
) )
} }
hiddenItems += hiddenShortcuts hiddenItems += hiddenShortcuts
appShortcutResults.value = shortcuts.applyRanking(resultOrder) appShortcutResults.value = shortcuts.applyRanking(query)
} else { } else {
appShortcutResults.value = emptyList() appShortcutResults.value = emptyList()
} }
@ -306,7 +305,7 @@ class SearchVM : ViewModel(), KoinComponent {
) )
} }
hiddenItems += hiddenFiles hiddenItems += hiddenFiles
fileResults.value = files.applyRanking(resultOrder) fileResults.value = files.applyRanking(query)
} else { } else {
fileResults.value = emptyList() fileResults.value = emptyList()
} }
@ -318,7 +317,7 @@ class SearchVM : ViewModel(), KoinComponent {
) )
} }
hiddenItems += hiddenContacts hiddenItems += hiddenContacts
contactResults.value = contacts.applyRanking(resultOrder) contactResults.value = contacts.applyRanking(query)
} else { } else {
contactResults.value = emptyList() contactResults.value = emptyList()
} }
@ -330,7 +329,7 @@ class SearchVM : ViewModel(), KoinComponent {
) )
} }
hiddenItems += hiddenEvents hiddenItems += hiddenEvents
calendarResults.value = events.applyRanking(resultOrder) calendarResults.value = events.applyRanking(query)
} else { } else {
calendarResults.value = emptyList() calendarResults.value = emptyList()
} }
@ -352,20 +351,20 @@ class SearchVM : ViewModel(), KoinComponent {
.distinctBy { it.key } .distinctBy { it.key }
.toList() .toList()
} else { } else {
locationResults.value = locations.applyRanking(resultOrder) locationResults.value = locations.applyRanking(query)
} }
} else { } else {
locationResults.value = emptyList() locationResults.value = emptyList()
} }
if (results.wikipedia != null) { if (results.wikipedia != null) {
articleResults.value = results.wikipedia!!.applyRanking(resultOrder) articleResults.value = results.wikipedia!!.applyRanking(query)
} else { } else {
articleResults.value = emptyList() articleResults.value = emptyList()
} }
if (results.websites != null) { if (results.websites != null) {
websiteResults.value = results.websites!!.applyRanking(resultOrder) websiteResults.value = results.websites!!.applyRanking(query)
} else { } else {
websiteResults.value = emptyList() websiteResults.value = emptyList()
} }
@ -398,7 +397,6 @@ class SearchVM : ViewModel(), KoinComponent {
} }
} }
} }
}
val missingCalendarPermission = combine( val missingCalendarPermission = combine(
permissionsManager.hasPermission(PermissionGroup.Calendar), permissionsManager.hasPermission(PermissionGroup.Calendar),
@ -469,7 +467,7 @@ class SearchVM : ViewModel(), KoinComponent {
expandedCategory.value = category expandedCategory.value = category
} }
private suspend fun <T : SavableSearchable> List<T>.applyRanking(order: SearchResultOrder): List<T> { private suspend fun <T : SavableSearchable> List<T>.applyRanking(query: String): List<T> {
if (size <= 1) return this if (size <= 1) return this
val sequence = asSequence() val sequence = asSequence()
val weights = searchableRepository.getWeights(map { it.key }).first() val weights = searchableRepository.getWeights(map { it.key }).first()
@ -477,10 +475,22 @@ class SearchVM : ViewModel(), KoinComponent {
val aWeight = weights[a.key] ?: 0.0 val aWeight = weights[a.key] ?: 0.0
val bWeight = weights[b.key] ?: 0.0 val bWeight = weights[b.key] ?: 0.0
val aScore = a.score.score * 0.7f + aWeight.toFloat() * 0.3f val aScore = if (a.score.isUnspecified) {
val bScore = b.score.score * 0.7f + bWeight.toFloat() * 0.3f 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() return sorted.distinctBy { it.key }.toList()
} }

View File

@ -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. * A total score for the result, combining the similarity with additional factors.
*/ */
val score: Float 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 { override fun compareTo(other: ResultScore): Int {
return score.compareTo(other.score) return score.compareTo(other.score)
@ -89,6 +89,15 @@ value class ResultScore private constructor(private val packed: Long) : Comparab
isPrimary = false, isPrimary = false,
similarity = 0f similarity = 0f
) )
}
val Unspecified = ResultScore(
isPrefix = false,
isSubstring = false,
isPrimary = false,
similarity = Float.NaN,
)
}
} }
inline val ResultScore.isUnspecified : Boolean
get() = this == ResultScore.Unspecified

View File

@ -4,5 +4,5 @@ import kotlinx.coroutines.Deferred
interface Searchable { interface Searchable {
val score: ResultScore val score: ResultScore
get() = ResultScore.Zero get() = ResultScore.Unspecified
} }

View File

@ -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? { private fun getActivityByComponentName(componentName: ComponentName?): LauncherApp? {
componentName ?: return null componentName ?: return null
val intent = Intent().setComponent(componentName) val intent = Intent().setComponent(componentName)

View File

@ -41,7 +41,7 @@ internal data class LauncherApp(
override val isSuspended: Boolean = false, override val isSuspended: Boolean = false,
internal val userSerialNumber: Long, internal val userSerialNumber: Long,
override val labelOverride: String? = null, override val labelOverride: String? = null,
override val score: ResultScore = ResultScore.Zero, override val score: ResultScore = ResultScore.Unspecified,
) : Application { ) : Application {
override val componentName: ComponentName override val componentName: ComponentName
@ -50,7 +50,7 @@ internal data class LauncherApp(
override val label: String = launcherActivityInfo.label.toString() 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, launcherActivityInfo,
versionName = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName), versionName = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
isSuspended = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SUSPENDED != 0, isSuspended = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SUSPENDED != 0,

View File

@ -14,6 +14,7 @@ import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
import de.mm20.launcher2.search.AppShortcut import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.ResultScore
import de.mm20.launcher2.search.SearchableRepository import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -141,22 +142,20 @@ internal class AppShortcutRepositoryImpl(
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
) )
val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle()) val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle())
?.filter { ?.mapNotNull {
if (it.longLabel != null) { val score = ResultScore(
return@filter matches(it.longLabel.toString(), query) query = query,
} primaryFields = listOfNotNull(it.longLabel?.toString(), it.shortLabel?.toString())
if (it.shortLabel != null) { )
return@filter matches(it.shortLabel.toString(), query) if (score.score < 0.8f) return@mapNotNull null
}
return@filter false
} ?: emptyList()
shortcuts.mapNotNull {
LauncherShortcut( LauncherShortcut(
context, context,
it it,
score
) )
}.toImmutableList() } ?: emptyList()
shortcuts.toImmutableList()
} else { } else {
persistentListOf() persistentListOf()

View File

@ -19,6 +19,7 @@ import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.AppShortcut import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.ResultScore
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -32,6 +33,7 @@ internal data class LauncherShortcut(
override val appName: String?, override val appName: String?,
internal val userSerialNumber: Long, internal val userSerialNumber: Long,
override val labelOverride: String? = null, override val labelOverride: String? = null,
override val score: ResultScore = ResultScore.Unspecified,
) : AppShortcut { ) : AppShortcut {
override val domain: String = Domain override val domain: String = Domain
@ -47,6 +49,7 @@ internal data class LauncherShortcut(
constructor( constructor(
context: Context, context: Context,
launcherShortcut: ShortcutInfo, launcherShortcut: ShortcutInfo,
score: ResultScore = ResultScore.Unspecified,
): this( ): this(
launcherShortcut = launcherShortcut, launcherShortcut = launcherShortcut,
appName = try { appName = try {
@ -55,7 +58,8 @@ internal data class LauncherShortcut(
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
null null
}, },
userSerialNumber = launcherShortcut.userHandle.getSerialNumber(context) userSerialNumber = launcherShortcut.userHandle.getSerialNumber(context),
score = score,
) )
override val label: String override val label: String

View File

@ -87,7 +87,7 @@ class PluginFileProvider(
uri = cursor[FileColumns.ContentUri]?.let { Uri.parse(it) } ?: continue, uri = cursor[FileColumns.ContentUri]?.let { Uri.parse(it) } ?: continue,
thumbnailUri = cursor[FileColumns.ThumbnailUri]?.let { Uri.parse(it) }, thumbnailUri = cursor[FileColumns.ThumbnailUri]?.let { Uri.parse(it) },
storageStrategy = config.storageStrategy, storageStrategy = config.storageStrategy,
isDirectory = cursor[FileColumns.IsDirectory] ?: false, isDirectory = cursor[FileColumns.IsDirectory] == true,
authority = pluginAuthority, authority = pluginAuthority,
timestamp = timestamp, timestamp = timestamp,
updatedSelf = { updatedSelf = {