Refactor search

This commit is contained in:
MM20 2024-07-17 23:27:00 +02:00
parent 5a28eb584e
commit c7804296d7
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
6 changed files with 351 additions and 207 deletions

View File

@ -1,7 +1,6 @@
package de.mm20.launcher2.ui.launcher.search package de.mm20.launcher2.ui.launcher.search
import android.content.Context import android.content.Context
import android.os.Process
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel 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.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.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Searchable 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.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
@ -36,17 +35,14 @@ import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -78,6 +74,7 @@ class SearchVM : ViewModel(), KoinComponent {
val locationResults = mutableStateOf<List<Location>>(emptyList()) val locationResults = mutableStateOf<List<Location>>(emptyList())
val appResults = mutableStateOf<List<Application>>(emptyList()) val appResults = mutableStateOf<List<Application>>(emptyList())
val workAppResults = mutableStateOf<List<Application>>(emptyList()) val workAppResults = mutableStateOf<List<Application>>(emptyList())
val privateSpaceAppResults = mutableStateOf<List<Application>>(emptyList())
val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList()) val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList())
val fileResults = mutableStateOf<List<File>>(emptyList()) val fileResults = mutableStateOf<List<File>>(emptyList())
val contactResults = mutableStateOf<List<Contact>>(emptyList()) val contactResults = mutableStateOf<List<Contact>>(emptyList())
@ -96,7 +93,11 @@ class SearchVM : ViewModel(), KoinComponent {
val showFilters = mutableStateOf(false) 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 filters = mutableStateOf(defaultFilters.value)
val filterBar = searchFilterSettings.filterBar val filterBar = searchFilterSettings.filterBar
val filterBarItems = searchFilterSettings.filterBarItems val filterBarItems = searchFilterSettings.filterBarItems
@ -174,146 +175,191 @@ class SearchVM : ViewModel(), KoinComponent {
} catch (_: CancellationException) { } catch (_: CancellationException) {
} }
hideFavorites.value = query.isNotEmpty() hideFavorites.value = query.isNotEmpty()
searchJob = viewModelScope.launch { searchJob = viewModelScope.launch {
searchUiSettings.resultOrder.collectLatest { resultOrder -> if (query.isEmpty()) {
searchService.search( val hiddenItemKeys = searchableRepository.getKeys(
query, maxVisibility = VisibilityLevel.SearchOnly,
filters = if (query.isEmpty()) filters.copy(apps = true) else filters, includeTypes = listOf("app"),
).collectLatest { results -> )
var resultsList = withContext(Dispatchers.Default) { val allApps = searchService.getAllApps()
listOfNotNull( fileResults.value = emptyList()
results.apps, contactResults.value = emptyList()
results.other, calendarResults.value = emptyList()
results.shortcuts, locationResults.value = emptyList()
results.files, articleResults.value = emptyList()
results.contacts, websiteResults.value = emptyList()
results.calendars, calculatorResults.value = emptyList()
results.locations, unitConverterResults.value = emptyList()
results.wikipedia, searchActionResults.value = emptyList()
results.websites,
results.calculators,
results.unitConverters,
results.searchActions,
).flatten()
.distinctBy { if (it is SavableSearchable) it.key else it }
.sortedBy { (it as? SavableSearchable) }
}
val relevance = allApps
if (query.isEmpty()) { .combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys }
emptyList() .collectLatest { (results, hiddenKeys) ->
} else { val hiddenItems = mutableListOf<SavableSearchable>()
val keys = resultsList.mapNotNull { (it as? SavableSearchable)?.key }
when (resultOrder) {
SearchResultOrder.LaunchCount -> searchableRepository.sortByRelevance( val (hiddenApps, apps) = results.standardProfileApps.partition {
keys hiddenKeys.contains(
).first() it.key
)
SearchResultOrder.Weighted -> searchableRepository.sortByWeight(
keys
).first()
else -> emptyList()
}
} }
hiddenItems += hiddenApps
resultsList = resultsList.sortedWith { a, b -> val (hiddenWorkApps, workApps) = results.workProfileApps.partition {
val lastLocation = devicePoseProvider.lastLocation hiddenKeys.contains(
when { it.key
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
} }
} hiddenItems += hiddenWorkApps
val hiddenItemKeys = searchableRepository.getKeys( val (hiddenPrivateApps, privateApps) = results.privateSpaceApps.partition {
maxVisibility = if (query.isEmpty()) VisibilityLevel.SearchOnly else VisibilityLevel.Hidden, hiddenKeys.contains(
) it.key
)
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() }
} }
hiddenItems += hiddenPrivateApps
appResults.value = apps appResults.value = apps
workAppResults.value = workApps workAppResults.value = workApps
appShortcutResults.value = shortcuts privateSpaceAppResults.value = privateApps
fileResults.value = files hiddenResults.value = hiddenItems
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
} }
} 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) { fun expandCategory(category: SearchCategory) {
expandedCategory.value = category 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 { enum class SearchCategory {
Apps, Apps,
Calculator, Calculator,

View File

@ -4,5 +4,5 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SearchableRepository<T : Searchable> { interface SearchableRepository<T : Searchable> {
fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<T>> fun search(query: String, allowNetwork: Boolean): Flow<List<T>>
} }

View File

@ -14,6 +14,7 @@ import androidx.core.content.getSystemService
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.plugin.data.get
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -107,6 +108,7 @@ class ProfileManager(
) )
) )
} }
Log.d("MM20", "Profiles: $profiles")
profileStates.value = profiles profileStates.value = profiles
} }
} }
@ -127,8 +129,9 @@ class ProfileManager(
* This only works when the launcher is installed in the primary profile. * This only works when the launcher is installed in the primary profile.
*/ */
private fun getProfileType(userHandle: UserHandle): Profile.Type { private fun getProfileType(userHandle: UserHandle): Profile.Type {
val restrictions = userManager.getUserRestrictions(userHandle)
return when { return when {
userManager.isManagedProfile(userHandle) -> Profile.Type.Work restrictions.getBoolean(UserManager.ALLOW_PARENT_PROFILE_APP_LINKING) -> Profile.Type.Work
userHandle == Process.myUserHandle() -> Profile.Type.Personal userHandle == Process.myUserHandle() -> Profile.Type.Personal
else -> Profile.Type.Private else -> Profile.Type.Private
} }
@ -137,7 +140,7 @@ class ProfileManager(
private fun getProfileState(userHandle: UserHandle): Profile.State { private fun getProfileState(userHandle: UserHandle): Profile.State {
return Profile.State( return Profile.State(
locked = !userManager.isUserUnlocked(userHandle), locked = !userManager.isUserUnlocked(userHandle),
hidden = !userManager.isUserUnlocked(userHandle), hidden = !userManager.isUserRunning(userHandle),
) )
} }
@ -151,18 +154,4 @@ class ProfileManager(
userManager.requestQuietModeEnabled(true, profile.userHandle) userManager.requestQuietModeEnabled(true, profile.userHandle)
} }
}
internal fun UserManager.isManagedProfile(userHandle: UserHandle): Boolean {
try {
val isManagedProfile = UserManager::class.java.getDeclaredMethod(
"isManagedProfile",
Int::class.javaPrimitiveType
)
val serial = getSerialNumberForUser(userHandle).toInt()
return isManagedProfile.invoke(this, serial) as Boolean
} catch (e: Exception) {
Log.e("MM20", "isManagedProfile could not be invoked", e)
return false
}
} }

View File

@ -53,6 +53,7 @@ dependencies {
implementation(project(":core:base")) implementation(project(":core:base"))
implementation(project(":core:preferences")) implementation(project(":core:preferences"))
implementation(project(":core:profiles"))
implementation(project(":core:crashreporter")) implementation(project(":core:crashreporter"))
implementation(project(":core:ktx")) implementation(project(":core:ktx"))
} }

View File

@ -18,6 +18,7 @@ val searchModule = module {
get(named<Website>()), get(named<Website>()),
get(), get(),
get(), get(),
get(),
) )
} }
} }

View File

@ -3,19 +3,24 @@ package de.mm20.launcher2.search
import de.mm20.launcher2.calculator.CalculatorRepository import de.mm20.launcher2.calculator.CalculatorRepository
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.data.customattrs.utils.withCustomLabels import de.mm20.launcher2.data.customattrs.utils.withCustomLabels
import de.mm20.launcher2.profiles.Profile
import de.mm20.launcher2.profiles.ProfileManager
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.searchactions.SearchActionService import de.mm20.launcher2.searchactions.SearchActionService
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.unitconverter.UnitConverterRepository import de.mm20.launcher2.unitconverter.UnitConverterRepository
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
@ -25,6 +30,8 @@ interface SearchService {
query: String, query: String,
filters: SearchFilters, filters: SearchFilters,
): Flow<SearchResults> ): Flow<SearchResults>
fun getAllApps(): Flow<AllAppsResults>
} }
internal class SearchServiceImpl( internal class SearchServiceImpl(
@ -40,14 +47,56 @@ internal class SearchServiceImpl(
private val websiteRepository: SearchableRepository<Website>, private val websiteRepository: SearchableRepository<Website>,
private val searchActionService: SearchActionService, private val searchActionService: SearchActionService,
private val customAttributesRepository: CustomAttributesRepository, private val customAttributesRepository: CustomAttributesRepository,
private val profileManager: ProfileManager,
) : SearchService { ) : SearchService {
override fun search( override fun search(
query: String, query: String,
filters: SearchFilters, filters: SearchFilters,
): Flow<SearchResults> = channelFlow { ): Flow<SearchResults> = flow {
val results = MutableStateFlow(SearchResults())
supervisorScope { supervisorScope {
val results = MutableStateFlow(SearchResults())
val customAttrResults = customAttributesRepository.search(query)
.map { items ->
val apps = mutableListOf<Application>()
val shortcuts = mutableListOf<AppShortcut>()
val contacts = mutableListOf<Contact>()
val events = mutableListOf<CalendarEvent>()
val files = mutableListOf<File>()
val unitConverters = mutableListOf<UnitConverter>()
val websites = mutableListOf<Website>()
val wikipedia = mutableListOf<Article>()
val locations = mutableListOf<Location>()
val searchActions = mutableListOf<SearchAction>()
for (it in items) {
when (it) {
is Application -> if (filters.apps) apps.add(it)
is AppShortcut -> if (filters.shortcuts) shortcuts.add(it)
is Contact -> if (filters.contacts) contacts.add(it)
is CalendarEvent -> if (filters.events) events.add(it)
is File -> if (filters.files) files.add(it)
is UnitConverter -> if (filters.tools) unitConverters.add(it)
is Website -> if (filters.websites) websites.add(it)
is Article -> if (filters.articles) wikipedia.add(it)
is Location -> if (filters.places) locations.add(it)
is SearchAction -> searchActions.add(it)
}
}
SearchResults(
apps = apps,
shortcuts = shortcuts,
contacts = contacts,
calendars = events,
files = files,
unitConverters = unitConverters,
websites = websites,
wikipedia = wikipedia,
locations = locations,
searchActions = searchActions,
)
}.shareIn(this, SharingStarted.WhileSubscribed(), 1)
launch { launch {
searchActionService.search(query) searchActionService.search(query)
.collectLatest { r -> .collectLatest { r ->
@ -59,10 +108,14 @@ internal class SearchServiceImpl(
if (filters.apps) { if (filters.apps) {
launch { launch {
appRepository.search(query, filters.allowNetwork) appRepository.search(query, filters.allowNetwork)
.combine(customAttrResults) { apps, customAttrs ->
if (customAttrs.apps != null) apps + customAttrs.apps
else apps
}
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(apps = r.toImmutableList()) it.copy(apps = r)
} }
} }
} }
@ -70,10 +123,14 @@ internal class SearchServiceImpl(
if (filters.shortcuts) { if (filters.shortcuts) {
launch { launch {
appShortcutRepository.search(query, filters.allowNetwork) appShortcutRepository.search(query, filters.allowNetwork)
.combine(customAttrResults) { shortcuts, customAttrs ->
if (customAttrs.shortcuts != null) shortcuts + customAttrs.shortcuts
else shortcuts
}
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(shortcuts = r.toImmutableList()) it.copy(shortcuts = r)
} }
} }
} }
@ -81,10 +138,14 @@ internal class SearchServiceImpl(
if (filters.contacts) { if (filters.contacts) {
launch { launch {
contactRepository.search(query, filters.allowNetwork) contactRepository.search(query, filters.allowNetwork)
.combine(customAttrResults) { contacts, customAttrs ->
if (customAttrs.contacts != null) contacts + customAttrs.contacts
else contacts
}
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(contacts = r.toImmutableList()) it.copy(contacts = r)
} }
} }
} }
@ -92,10 +153,14 @@ internal class SearchServiceImpl(
if (filters.events) { if (filters.events) {
launch { launch {
calendarRepository.search(query, filters.allowNetwork) calendarRepository.search(query, filters.allowNetwork)
.combine(customAttrResults) { calendars, customAttrs ->
if (customAttrs.calendars != null) calendars + customAttrs.calendars
else calendars
}
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(calendars = r.toImmutableList()) it.copy(calendars = r)
} }
} }
} }
@ -104,8 +169,8 @@ internal class SearchServiceImpl(
launch { launch {
calculatorRepository.search(query).collectLatest { r -> calculatorRepository.search(query).collectLatest { r ->
results.update { results.update {
it.copy(calculators = r?.let { persistentListOf(it) } it.copy(calculators = r?.let { listOf(it) }
?: persistentListOf()) ?: listOf())
} }
} }
} }
@ -113,8 +178,8 @@ internal class SearchServiceImpl(
unitConverterRepository.search(query) unitConverterRepository.search(query)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(unitConverters = r?.let { persistentListOf(it) } it.copy(unitConverters = r?.let { listOf(it) }
?: persistentListOf()) ?: listOf())
} }
} }
} }
@ -122,10 +187,14 @@ internal class SearchServiceImpl(
if (filters.websites) { if (filters.websites) {
launch { launch {
websiteRepository.search(query, filters.allowNetwork) websiteRepository.search(query, filters.allowNetwork)
.combine(customAttrResults) { websites, customAttrs ->
if (customAttrs.websites != null) websites + customAttrs.websites
else websites
}
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(websites = r.toImmutableList()) it.copy(websites = r)
} }
} }
} }
@ -134,10 +203,14 @@ internal class SearchServiceImpl(
launch { launch {
delay(750) delay(750)
articleRepository.search(query, filters.allowNetwork) articleRepository.search(query, filters.allowNetwork)
.combine(customAttrResults) { articles, customAttrs ->
if (customAttrs.wikipedia != null) articles + customAttrs.wikipedia
else articles
}
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(wikipedia = r.toImmutableList()) it.copy(wikipedia = r)
} }
} }
} }
@ -145,10 +218,14 @@ internal class SearchServiceImpl(
if (filters.places) { if (filters.places) {
launch { launch {
locationRepository.search(query, filters.allowNetwork) locationRepository.search(query, filters.allowNetwork)
.combine(customAttrResults) { locations, customAttrs ->
if (customAttrs.locations != null) locations + customAttrs.locations
else locations
}
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(locations = r.toImmutableList()) it.copy(locations = r)
} }
} }
} }
@ -159,56 +236,65 @@ internal class SearchServiceImpl(
query, query,
filters.allowNetwork filters.allowNetwork
) )
.combine(customAttrResults) { files, customAttrs ->
if (customAttrs.files != null) files + customAttrs.files
else files
}
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(files = r.toImmutableList()) it.copy(files = r)
} }
} }
} }
} }
launch { emitAll(results)
customAttributesRepository.search(query) }
.withCustomLabels(customAttributesRepository) }
.collectLatest { r ->
results.update {
it.copy(other = r
.filter {
filters.apps && it is Application ||
filters.shortcuts && it is AppShortcut ||
filters.contacts && it is Contact ||
filters.events && it is CalendarEvent ||
filters.files && it is File ||
filters.websites && it is Website ||
filters.articles && it is Article ||
filters.places && it is Location
}
.toImmutableList()
)
}
}
}
launch {
results.collectLatest { send(it) }
}
override fun getAllApps(): Flow<AllAppsResults> {
return profileManager.activeProfiles.flatMapLatest { profiles ->
val standardProfile = profiles.find { it.type == Profile.Type.Personal }
val workProfile = profiles.find { it.type == Profile.Type.Work }
val privateSpace = profiles.find { it.type == Profile.Type.Private }
appRepository.search("", false)
.withCustomLabels(customAttributesRepository)
.map {
val grouped = it.groupBy { it.user }
val standardProfileApps =
standardProfile?.let { grouped[it.userHandle] } ?: emptyList()
val workProfileApps = workProfile?.let { grouped[it.userHandle] } ?: emptyList()
val privateSpaceApps =
privateSpace?.let { grouped[it.userHandle] } ?: emptyList()
AllAppsResults(
standardProfileApps = standardProfileApps.sorted(),
workProfileApps = workProfileApps.sorted(),
privateSpaceApps = privateSpaceApps.sorted(),
)
}
} }
} }
} }
data class SearchResults( data class SearchResults(
val apps: ImmutableList<Application>? = null, val apps: List<Application>? = null,
val shortcuts: ImmutableList<AppShortcut>? = null, val shortcuts: List<AppShortcut>? = null,
val contacts: ImmutableList<Contact>? = null, val contacts: List<Contact>? = null,
val calendars: ImmutableList<CalendarEvent>? = null, val calendars: List<CalendarEvent>? = null,
val files: ImmutableList<File>? = null, val files: List<File>? = null,
val calculators: ImmutableList<Calculator>? = null, val calculators: List<Calculator>? = null,
val unitConverters: ImmutableList<UnitConverter>? = null, val unitConverters: List<UnitConverter>? = null,
val websites: ImmutableList<Website>? = null, val websites: List<Website>? = null,
val wikipedia: ImmutableList<Article>? = null, val wikipedia: List<Article>? = null,
val locations: ImmutableList<Location>? = null, val locations: List<Location>? = null,
val searchActions: ImmutableList<SearchAction>? = null, val searchActions: List<SearchAction>? = null,
val other: ImmutableList<SavableSearchable>? = null, )
data class AllAppsResults(
val standardProfileApps: List<Application>,
val workProfileApps: List<Application>,
val privateSpaceApps: List<Application>,
) )
fun SearchResults.toList(): List<Searchable> { fun SearchResults.toList(): List<Searchable> {
@ -223,6 +309,5 @@ fun SearchResults.toList(): List<Searchable> {
websites, websites,
wikipedia, wikipedia,
searchActions, searchActions,
other,
).flatten() ).flatten()
} }