Add API to control online search

This commit is contained in:
MM20 2024-01-28 21:35:36 +01:00
parent d7340c485b
commit 426473e397
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
20 changed files with 44 additions and 36 deletions

View File

@ -40,6 +40,7 @@ class SearchablePickerVM : ViewModel(), KoinComponent {
searchJob = viewModelScope.launch { searchJob = viewModelScope.launch {
searchService.search( searchService.search(
query = query, query = query,
allowNetwork = true,
).collectLatest { ).collectLatest {
if (searchQuery != query) return@collectLatest if (searchQuery != query) return@collectLatest
items = withContext(Dispatchers.Default) { items = withContext(Dispatchers.Default) {

View File

@ -127,7 +127,8 @@ class SearchVM : ViewModel(), KoinComponent {
searchJob = viewModelScope.launch { searchJob = viewModelScope.launch {
searchUiSettings.resultOrder.collectLatest { resultOrder -> searchUiSettings.resultOrder.collectLatest { resultOrder ->
searchService.search( searchService.search(
query query,
allowNetwork = true,
).collectLatest { results -> ).collectLatest { results ->
var resultsList = withContext(Dispatchers.Default) { var resultsList = withContext(Dispatchers.Default) {
listOfNotNull( listOfNotNull(

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): Flow<ImmutableList<T>> fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<T>>
} }

View File

@ -5,5 +5,6 @@ abstract class SearchPluginContract {
const val Search = "search" const val Search = "search"
const val Root = "root" const val Root = "root"
const val QueryParam = "query" const val QueryParam = "query"
const val AllowNetworkParam = "network"
} }
} }

View File

@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.map
class FakeAppRepository(private val context: Context, private val fakePackages: Int) : SearchableRepository<Application> { class FakeAppRepository(private val context: Context, private val fakePackages: Int) : SearchableRepository<Application> {
override fun search(query: String): Flow<ImmutableList<Application>> { override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<Application>> {
return if (query.isEmpty()) { return if (query.isEmpty()) {
buildList { buildList {
repeat(fakePackages) { repeat(fakePackages) {

View File

@ -29,8 +29,6 @@ import org.apache.commons.text.similarity.FuzzyScore
import java.util.Locale import java.util.Locale
interface AppRepository : SearchableRepository<Application> { interface AppRepository : SearchableRepository<Application> {
override fun search(query: String): Flow<ImmutableList<Application>>
fun findMany(): Flow<ImmutableList<Application>> fun findMany(): Flow<ImmutableList<Application>>
} }
@ -197,7 +195,7 @@ internal class AppRepositoryImpl(
return installedApps.map { it.toImmutableList() } return installedApps.map { it.toImmutableList() }
} }
override fun search(query: String): Flow<ImmutableList<LauncherApp>> { override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<LauncherApp>> {
return installedApps.map { apps -> return installedApps.map { apps ->
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val appResults = mutableListOf<LauncherApp>() val appResults = mutableListOf<LauncherApp>()

View File

@ -111,7 +111,7 @@ internal class AppShortcutRepositoryImpl(
return flags return flags
} }
override fun search(query: String): Flow<ImmutableList<AppShortcut>> { override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<AppShortcut>> {
if (query.length < 3) { if (query.length < 3) {
return flowOf(persistentListOf()) return flowOf(persistentListOf())
} }

View File

@ -40,7 +40,7 @@ internal class CalendarRepositoryImpl(
private val settings: CalendarSearchSettings, private val settings: CalendarSearchSettings,
) : CalendarRepository { ) : CalendarRepository {
override fun search(query: String): Flow<ImmutableList<CalendarEvent>> { override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<CalendarEvent>> {
if (query.isBlank() || query.length < 2) { if (query.isBlank() || query.length < 2) {
return flow { return flow {
emit(persistentListOf()) emit(persistentListOf())

View File

@ -164,7 +164,7 @@ internal class ContactRepository(
) )
} }
override fun search(query: String): Flow<ImmutableList<Contact>> { override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<Contact>> {
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts) val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
if (query.length < 2) { if (query.length < 2) {

View File

@ -32,6 +32,7 @@ internal class FileRepository(
override fun search( override fun search(
query: String, query: String,
allowNetwork: Boolean,
) = channelFlow { ) = channelFlow {
if (query.isBlank()) { if (query.isBlank()) {
send(persistentListOf()) send(persistentListOf())
@ -55,7 +56,7 @@ internal class FileRepository(
} }
val results = mutableListOf<File>() val results = mutableListOf<File>()
for (provider in providers) { for (provider in providers) {
results.addAll(provider.search(query)) results.addAll(provider.search(query, allowNetwork))
send(results.toImmutableList()) send(results.toImmutableList())
} }
} }

View File

@ -3,5 +3,5 @@ package de.mm20.launcher2.files.providers
import de.mm20.launcher2.search.File import de.mm20.launcher2.search.File
internal interface FileProvider { internal interface FileProvider {
suspend fun search(query: String): List<File> suspend fun search(query: String, allowNetwork: Boolean): List<File>
} }

View File

@ -12,8 +12,8 @@ import kotlinx.collections.immutable.toImmutableMap
internal class GDriveFileProvider( internal class GDriveFileProvider(
private val context: Context private val context: Context
) : FileProvider { ) : FileProvider {
override suspend fun search(query: String): List<File> { override suspend fun search(query: String, allowNetwork: Boolean): List<File> {
if (query.length < 4) return emptyList() if (query.length < 4 || !allowNetwork) return emptyList()
val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query) val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query)
return driveFiles.map { return driveFiles.map {
GDriveFile( GDriveFile(

View File

@ -13,7 +13,7 @@ internal class LocalFileProvider(
private val context: Context, private val context: Context,
private val permissionsManager: PermissionsManager private val permissionsManager: PermissionsManager
): FileProvider { ): FileProvider {
override suspend fun search(query: String): List<File> = withContext(Dispatchers.IO) { override suspend fun search(query: String, allowNetwork: Boolean): List<File> = withContext(Dispatchers.IO) {
if (!permissionsManager.checkPermissionOnce(PermissionGroup.ExternalStorage)) { if (!permissionsManager.checkPermissionOnce(PermissionGroup.ExternalStorage)) {
return@withContext emptyList() return@withContext emptyList()
} }

View File

@ -12,8 +12,8 @@ import kotlin.math.min
internal class NextcloudFileProvider( internal class NextcloudFileProvider(
private val nextcloudClient: NextcloudApiHelper private val nextcloudClient: NextcloudApiHelper
) : FileProvider { ) : FileProvider {
override suspend fun search(query: String): List<File> { override suspend fun search(query: String, allowNetwork: Boolean): List<File> {
if (query.length < 4) return emptyList() if (query.length < 4 || !allowNetwork) return emptyList()
val server = nextcloudClient.getServer() ?: return emptyList() val server = nextcloudClient.getServer() ?: return emptyList()
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
nextcloudClient.files.search(query).let { it.subList(0, min(10, it.size)) }.map { nextcloudClient.files.search(query).let { it.subList(0, min(10, it.size)) }.map {

View File

@ -9,8 +9,8 @@ import kotlinx.collections.immutable.persistentMapOf
internal class OwncloudFileProvider( internal class OwncloudFileProvider(
private val owncloudClient: OwncloudClient private val owncloudClient: OwncloudClient
) : FileProvider { ) : FileProvider {
override suspend fun search(query: String): List<File> { override suspend fun search(query: String, allowNetwork: Boolean): List<File> {
if (query.length < 4) return emptyList() if (query.length < 4 || !allowNetwork) return emptyList()
val server = owncloudClient.getServer() ?: return emptyList() val server = owncloudClient.getServer() ?: return emptyList()
return owncloudClient.files.query(query).map { return owncloudClient.files.query(query).map {
OwncloudFile( OwncloudFile(

View File

@ -26,12 +26,13 @@ class PluginFileProvider(
private val context: Context, private val context: Context,
private val pluginAuthority: String, private val pluginAuthority: String,
) : FileProvider { ) : FileProvider {
override suspend fun search(query: String): List<File> = withContext(Dispatchers.IO) { override suspend fun search(query: String, allowNetwork: Boolean): List<File> = withContext(Dispatchers.IO) {
val uri = Uri.Builder() val uri = Uri.Builder()
.scheme("content") .scheme("content")
.authority(pluginAuthority) .authority(pluginAuthority)
.path(SearchPluginContract.Paths.Search) .path(SearchPluginContract.Paths.Search)
.appendQueryParameter(SearchPluginContract.Paths.QueryParam, query) .appendQueryParameter(SearchPluginContract.Paths.QueryParam, query)
.appendQueryParameter(SearchPluginContract.Paths.AllowNetworkParam, allowNetwork.toString())
.build() .build()
val cancellationSignal = CancellationSignal() val cancellationSignal = CancellationSignal()

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.websites
import android.content.Context import android.content.Context
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.compose.runtime.Immutable
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import de.mm20.launcher2.preferences.search.WebsiteSearchSettings import de.mm20.launcher2.preferences.search.WebsiteSearchSettings
import de.mm20.launcher2.search.SearchableRepository import de.mm20.launcher2.search.SearchableRepository
@ -11,8 +10,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl import okhttp3.HttpUrl
@ -20,7 +18,6 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.UncheckedIOException import org.jsoup.UncheckedIOException
import org.koin.core.component.KoinComponent
import java.io.IOException import java.io.IOException
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URISyntaxException import java.net.URISyntaxException
@ -40,7 +37,8 @@ internal class WebsiteRepository(
.writeTimeout(1000, TimeUnit.MILLISECONDS) .writeTimeout(1000, TimeUnit.MILLISECONDS)
.build() .build()
override fun search(query: String): Flow<ImmutableList<Website>> { override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<Website>> {
if (!allowNetwork) return flowOf(persistentListOf())
return settings.enabled.transformLatest { enabled -> return settings.enabled.transformLatest { enabled ->
emit(persistentListOf()) emit(persistentListOf())
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@ -53,8 +53,8 @@ internal class WikipediaRepository(
private lateinit var wikipediaService: WikipediaApi private lateinit var wikipediaService: WikipediaApi
override fun search(query: String): Flow<ImmutableList<Wikipedia>> { override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<Wikipedia>> {
if (query.length < 4) return flowOf(persistentListOf()) if (query.length < 4 || !allowNetwork) return flowOf(persistentListOf())
return settings.enabled.transformLatest { return settings.enabled.transformLatest {
emit(persistentListOf()) emit(persistentListOf())

View File

@ -19,7 +19,7 @@ abstract class SearchPluginProvider<T>(
* Search for items matching the given query * Search for items matching the given query
* @param query The query to search for * @param query The query to search for
*/ */
abstract suspend fun search(query: String): List<T> abstract suspend fun search(query: String, allowNetwork: Boolean): List<T>
abstract suspend fun get(id: String): T? abstract suspend fun get(id: String): T?
override fun onCreate(): Boolean { override fun onCreate(): Boolean {
@ -48,7 +48,10 @@ abstract class SearchPluginProvider<T>(
uri.pathSegments.size == 1 && uri.pathSegments.first() == SearchPluginContract.Paths.Search -> { uri.pathSegments.size == 1 && uri.pathSegments.first() == SearchPluginContract.Paths.Search -> {
val query = val query =
uri.getQueryParameter(SearchPluginContract.Paths.QueryParam) ?: return null uri.getQueryParameter(SearchPluginContract.Paths.QueryParam) ?: return null
val results = search(query, cancellationSignal) val allowNetwork =
uri.getQueryParameter(SearchPluginContract.Paths.AllowNetworkParam)?.toBoolean()
?: false
val results = search(query, allowNetwork, cancellationSignal)
val cursor = createCursor(results.size) val cursor = createCursor(results.size)
for (result in results) { for (result in results) {
writeToCursor(cursor, result) writeToCursor(cursor, result)
@ -95,10 +98,11 @@ abstract class SearchPluginProvider<T>(
private fun search( private fun search(
query: String, query: String,
allowNetwork: Boolean,
cancellationSignal: CancellationSignal? cancellationSignal: CancellationSignal?
): List<T> { ): List<T> {
return launchWithCancellationSignal(cancellationSignal) { return launchWithCancellationSignal(cancellationSignal) {
search(query) search(query, allowNetwork)
} }
} }

View File

@ -23,6 +23,7 @@ import kotlinx.coroutines.supervisorScope
interface SearchService { interface SearchService {
fun search( fun search(
query: String, query: String,
allowNetwork: Boolean = false,
): Flow<SearchResults> ): Flow<SearchResults>
} }
@ -42,6 +43,7 @@ internal class SearchServiceImpl(
override fun search( override fun search(
query: String, query: String,
allowNetwork: Boolean,
): Flow<SearchResults> = channelFlow { ): Flow<SearchResults> = channelFlow {
val results = MutableStateFlow(SearchResults()) val results = MutableStateFlow(SearchResults())
supervisorScope { supervisorScope {
@ -54,7 +56,7 @@ internal class SearchServiceImpl(
} }
} }
launch { launch {
appRepository.search(query) appRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
@ -63,7 +65,7 @@ internal class SearchServiceImpl(
} }
} }
launch { launch {
appShortcutRepository.search(query) appShortcutRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
@ -72,7 +74,7 @@ internal class SearchServiceImpl(
} }
} }
launch { launch {
contactRepository.search(query) contactRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
@ -81,7 +83,7 @@ internal class SearchServiceImpl(
} }
} }
launch { launch {
calendarRepository.search(query) calendarRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
@ -107,7 +109,7 @@ internal class SearchServiceImpl(
} }
} }
launch { launch {
websiteRepository.search(query) websiteRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
@ -117,7 +119,7 @@ internal class SearchServiceImpl(
} }
launch { launch {
delay(750) delay(750)
articleRepository.search(query) articleRepository.search(query, allowNetwork)
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
@ -128,6 +130,7 @@ internal class SearchServiceImpl(
launch { launch {
fileRepository.search( fileRepository.search(
query, query,
allowNetwork
) )
.withCustomLabels(customAttributesRepository) .withCustomLabels(customAttributesRepository)
.collectLatest { r -> .collectLatest { r ->