Change search result order for all other categories
This commit is contained in:
parent
bf6e963545
commit
774777f79c
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -4,5 +4,5 @@ import kotlinx.coroutines.Deferred
|
||||
|
||||
interface Searchable {
|
||||
val score: ResultScore
|
||||
get() = ResultScore.Zero
|
||||
get() = ResultScore.Unspecified
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user