Add API to control online search
This commit is contained in:
parent
d7340c485b
commit
426473e397
@ -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,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) {
|
||||||
|
|||||||
@ -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 ->
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user