Add initial support for custom search engines

Will expose pref UI later
This commit is contained in:
SuperDragonXD
2024-06-28 22:16:32 +08:00
parent 4565d48ea3
commit 4826650eef
5 changed files with 143 additions and 33 deletions
@@ -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),
)
@@ -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)
}
@@ -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,
@@ -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<String> = 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<String>
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<String> = withContext(Dispatchers.IO) {
if (query.isEmpty() || query.isBlank() || max <= 0) {
return@withContext emptyList()
}
try {
val response: Response<ResponseBody> = 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<String>()
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<ResponseBody> = 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<String> = withContext(Dispatchers.IO) {
if (query.isEmpty() || query.isBlank() || max <= 0) {
return@withContext emptyList()
}
try {
val response: Response<ResponseBody> = 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<ResponseBody>
}
/**
* 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,
@@ -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),