Add API to control online search

This commit is contained in:
MM20
2024-01-28 21:35:36 +01:00
parent d7340c485b
commit 426473e397
20 changed files with 44 additions and 36 deletions
@@ -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) {
@@ -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(
@@ -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>>
} }
@@ -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"
} }
} }
@@ -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) {
@@ -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>()
@@ -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())
} }
@@ -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())
@@ -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) {
@@ -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())
} }
} }
@@ -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>
} }
@@ -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(
@@ -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()
} }
@@ -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 {
@@ -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(
@@ -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()
@@ -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,8 +37,9 @@ 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>> {
return settings.enabled.transformLatest {enabled -> if (!allowNetwork) return flowOf(persistentListOf())
return settings.enabled.transformLatest { enabled ->
emit(persistentListOf()) emit(persistentListOf())
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
httpClient.dispatcher.cancelAll() httpClient.dispatcher.cancelAll()
@@ -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())
@@ -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)
} }
} }
@@ -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 ->