From 4826650eefdbfb8ea1758bbdf399a9eed2fe6e4a Mon Sep 17 00:00:00 2001 From: SuperDragonXD <70206496+SuperDragonXD@users.noreply.github.com> Date: Fri, 28 Jun 2024 22:16:32 +0800 Subject: [PATCH] Add initial support for custom search engines Will expose pref UI later --- .../preferences2/PreferenceManager2.kt | 2 +- .../search/adapter/SearchTargetCompat.kt | 2 + .../LawnchairLocalSearchAlgorithm.kt | 7 +- .../lawnchair/search/algorithms/data/Web.kt | 163 +++++++++++++++--- .../destinations/SearchPreferences.kt | 2 +- 5 files changed, 143 insertions(+), 33 deletions(-) diff --git a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt index 573ac47264..7b10d54b9e 100644 --- a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt +++ b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt @@ -402,7 +402,7 @@ class PreferenceManager2 private constructor(private val context: Context) : Pre defaultValue = resourceProvider.getInt(R.dimen.config_default_search_max_result_count), ) - val maxSuggestionResultCount = preference( + val maxWebSuggestionResultCount = preference( key = intPreferencesKey(name = "max_suggestion_result_count"), defaultValue = resourceProvider.getInt(R.dimen.config_default_suggestion_max_result_count), ) diff --git a/lawnchair/src/app/lawnchair/search/adapter/SearchTargetCompat.kt b/lawnchair/src/app/lawnchair/search/adapter/SearchTargetCompat.kt index 0c46b6fe47..f2e6f3f500 100644 --- a/lawnchair/src/app/lawnchair/search/adapter/SearchTargetCompat.kt +++ b/lawnchair/src/app/lawnchair/search/adapter/SearchTargetCompat.kt @@ -275,6 +275,7 @@ data class SearchTargetCompat( RESULT_TYPE_FILE_TILE, RESULT_TYPE_SETTING_TILE, RESULT_TYPE_CALCULATOR, + RESULT_TYPE_COMPOSE_VIEW, ], ) @Retention(AnnotationRetention.SOURCE) @@ -292,6 +293,7 @@ data class SearchTargetCompat( const val RESULT_TYPE_FILE_TILE = 1 shl 8 const val RESULT_TYPE_SETTING_TILE = 1 shl 9 const val RESULT_TYPE_CALCULATOR = 1 shl 10 + const val RESULT_TYPE_COMPOSE_VIEW = 1 shl 11 fun wrap(target: SearchTarget): SearchTargetCompat = SearchTargetCompat(target) } diff --git a/lawnchair/src/app/lawnchair/search/algorithms/LawnchairLocalSearchAlgorithm.kt b/lawnchair/src/app/lawnchair/search/algorithms/LawnchairLocalSearchAlgorithm.kt index f03d9242c2..08cc3cf701 100644 --- a/lawnchair/src/app/lawnchair/search/algorithms/LawnchairLocalSearchAlgorithm.kt +++ b/lawnchair/src/app/lawnchair/search/algorithms/LawnchairLocalSearchAlgorithm.kt @@ -23,11 +23,11 @@ import app.lawnchair.search.algorithms.data.ContactInfo import app.lawnchair.search.algorithms.data.IFileInfo import app.lawnchair.search.algorithms.data.RecentKeyword import app.lawnchair.search.algorithms.data.SettingInfo +import app.lawnchair.search.algorithms.data.WebSearchProvider import app.lawnchair.search.algorithms.data.calculateEquationFromString import app.lawnchair.search.algorithms.data.findContactsByName import app.lawnchair.search.algorithms.data.findSettingsByNameAndAction import app.lawnchair.search.algorithms.data.getRecentKeyword -import app.lawnchair.search.algorithms.data.getStartPageSuggestions import app.lawnchair.search.algorithms.data.queryFilesInMediaStore import app.lawnchair.util.checkAndRequestFilesPermission import app.lawnchair.util.isDefaultLauncher @@ -48,6 +48,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -103,7 +104,7 @@ class LawnchairLocalSearchAlgorithm(context: Context) : LawnchairSearchAlgorithm pref2.maxPeopleResultCount.onEach(launchIn = coroutineScope) { maxPeopleCount = it } - pref2.maxSuggestionResultCount.onEach(launchIn = coroutineScope) { + pref2.maxWebSuggestionResultCount.onEach(launchIn = coroutineScope) { maxWebSuggestionsCount = it } pref2.maxSettingsEntryResultCount.onEach(launchIn = coroutineScope) { @@ -350,7 +351,7 @@ class LawnchairLocalSearchAlgorithm(context: Context) : LawnchairSearchAlgorithm val timeout = maxWebSuggestionDelay.toLong() val result = withTimeoutOrNull(timeout) { if (prefs.searchResultStartPageSuggestion.get()) { - getStartPageSuggestions(query, maxWebSuggestionsCount).map { + WebSearchProvider.fromString("startpage").getSuggestions(query, maxWebSuggestionsCount).map { SearchResult( WEB_SUGGESTION, it, diff --git a/lawnchair/src/app/lawnchair/search/algorithms/data/Web.kt b/lawnchair/src/app/lawnchair/search/algorithms/data/Web.kt index 8636f0be98..b9220c410b 100644 --- a/lawnchair/src/app/lawnchair/search/algorithms/data/Web.kt +++ b/lawnchair/src/app/lawnchair/search/algorithms/data/Web.kt @@ -14,44 +14,151 @@ import retrofit2.create import retrofit2.http.GET import retrofit2.http.Query -private val retrofit = Retrofit.Builder() - .baseUrl("https://www.startpage.com/") - .addConverterFactory(kotlinxJson.asConverterFactory("application/json".toMediaType())) - .build() +// TODO: Create preferences UI -val startPageService: StartPageService = retrofit.create() +/** + * A class to get the current web search provider + */ +sealed class WebSearchProvider { -suspend fun getStartPageSuggestions(query: String, max: Int): List = withContext(Dispatchers.IO) { - if (query.isEmpty() || query.isBlank() || max <= 0) { - return@withContext emptyList() + /** + * [Retrofit] instance used for searching. + */ + abstract val retrofit: Retrofit + + /** + * The search service to use. + */ + abstract val service: GenericSearchService + + /** + * Suspending function to get the list of suggestions from the current suggestion + * @param query The input text + * @param max The maximum number of items + * @return The list of suggestions + */ + abstract suspend fun getSuggestions(query: String, max: Int): List + + data object Google : WebSearchProvider() { + override val retrofit: Retrofit + get() = Retrofit.Builder() + .baseUrl("https://www.google.com/") + .addConverterFactory(kotlinxJson.asConverterFactory("application/json".toMediaType())) + .build() + + override val service: GoogleService + get() = retrofit.create() + + override suspend fun getSuggestions(query: String, max: Int): List = withContext(Dispatchers.IO) { + if (query.isEmpty() || query.isBlank() || max <= 0) { + return@withContext emptyList() + } + + try { + val response: Response = service.getSuggestions(query = query) + + if (response.isSuccessful) { + val responseBody = response.body()?.string() ?: return@withContext emptyList() + + val jsonPayload = Regex("\\((.*)\\)").find(responseBody)?.groupValues?.get(1) + + // Manual JSON parsing + val jsonArray = JSONArray(jsonPayload) + val suggestionsArray = jsonArray.getJSONArray(1) // Get the suggestions array + val suggestionsList = mutableListOf() + for (i in 0 until suggestionsArray.length().coerceAtMost(max)) { + suggestionsList.add(suggestionsArray.getString(i)) + } + return@withContext suggestionsList + } else { + Log.d("Failed to retrieve suggestions", ": ${response.code()}") + return@withContext emptyList() + } + } catch (e: Exception) { + Log.e("Exception", "Error during suggestion retrieval: ${e.message}") + return@withContext emptyList() + } + } } - try { - val response: Response = startPageService.getStartPageSuggestions( - query = query, - segment = "startpage.lawnchair", - partner = "lawnchair", - format = "opensearch", - ) + /** + * A Google-like search engine. + */ + data object StartPage : WebSearchProvider() { + override val retrofit: Retrofit = Retrofit.Builder() + .baseUrl("https://www.startpage.com/") + .addConverterFactory(kotlinxJson.asConverterFactory("application/json".toMediaType())) + .build() - if (response.isSuccessful) { - val responseBody = response.body()?.string() - return@withContext JSONArray(responseBody).optJSONArray(1)?.let { array -> - (0 until array.length()).take(max).map { array.getString(it) } - } ?: emptyList() - } else { - Log.d("Failed to retrieve suggestions", ": ${response.code()}") - return@withContext emptyList() + override val service: StartPageService = retrofit.create() + + override suspend fun getSuggestions(query: String, max: Int): List = withContext(Dispatchers.IO) { + if (query.isEmpty() || query.isBlank() || max <= 0) { + return@withContext emptyList() + } + + try { + val response: Response = service.getSuggestions( + query = query, + segment = "startpage.lawnchair", + partner = "lawnchair", + format = "opensearch", + ) + + if (response.isSuccessful) { + val responseBody = response.body()?.string() + return@withContext JSONArray(responseBody).optJSONArray(1)?.let { array -> + (0 until array.length()).take(max).map { array.getString(it) } + } ?: emptyList() + } else { + Log.d("Failed to retrieve suggestions", ": ${response.code()}") + return@withContext emptyList() + } + } catch (e: Exception) { + Log.e("Exception", "Error during suggestion retrieval: ${e.message}") + return@withContext emptyList() + } } - } catch (e: Exception) { - Log.e("Exception", "Error during suggestion retrieval: ${e.message}") - return@withContext emptyList() + + override fun toString() = "startpage" + } + + companion object { + fun fromString(value: String): WebSearchProvider = when (value) { + "google" -> Google + else -> StartPage + } + + fun values() = listOf( + Google, + StartPage, + ) } } -interface StartPageService { +/** + * Provides an interface for getting search suggestions from the web. + */ +interface GenericSearchService + +/** + * Web suggestions for [WebSearchProvider.Google] + */ +interface GoogleService : GenericSearchService { + @GET("complete/search") + suspend fun getSuggestions( + @Query("client") client: String = "firefox", + @Query("q") query: String, + @Query("callback") callback: String = "json" + ): Response +} + +/** + * Web suggestions for [WebSearchProvider.StartPage]. + */ +interface StartPageService : GenericSearchService { @GET("suggestions") - suspend fun getStartPageSuggestions( + suspend fun getSuggestions( @Query("q") query: String, @Query("segment") segment: String, @Query("partner") partner: String, diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/SearchPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/SearchPreferences.kt index 9fbec717e5..a9364e8ad1 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/destinations/SearchPreferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/SearchPreferences.kt @@ -158,7 +158,7 @@ private fun LocalSearchSettings( SearchSuggestionPreference( adapter = prefs.searchResultStartPageSuggestion.getAdapter(), - maxCountAdapter = prefs2.maxSuggestionResultCount.getAdapter(), + maxCountAdapter = prefs2.maxWebSuggestionResultCount.getAdapter(), maxCountRange = 3..10, label = stringResource(id = R.string.search_pref_result_web_title), maxCountLabel = stringResource(id = R.string.max_suggestion_result_count_title),