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.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<SavableSearchable>()
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))
}
.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 <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
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()
}

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.
*/
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
)
}
}
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 {
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? {
componentName ?: return null
val intent = Intent().setComponent(componentName)

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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 = {