Add initial support for custom search engines
Will expose pref UI later
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user