Refactor search
This commit is contained in:
parent
5a28eb584e
commit
c7804296d7
@ -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,
|
||||||
|
|||||||
@ -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>>
|
||||||
}
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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"))
|
||||||
}
|
}
|
||||||
@ -18,6 +18,7 @@ val searchModule = module {
|
|||||||
get(named<Website>()),
|
get(named<Website>()),
|
||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
|
get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user