diff --git a/lawnchair/res/drawable/ic_kagi.xml b/lawnchair/res/drawable/ic_kagi.xml new file mode 100644 index 0000000000..f69f350a57 --- /dev/null +++ b/lawnchair/res/drawable/ic_kagi.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/lawnchair/res/drawable/ic_kagi_tinted.xml b/lawnchair/res/drawable/ic_kagi_tinted.xml new file mode 100644 index 0000000000..c5cb145771 --- /dev/null +++ b/lawnchair/res/drawable/ic_kagi_tinted.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index 4ff97a41c3..7fc90aa95f 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -644,6 +644,7 @@ Firefox Iceraven Mull + Kagi %1$s and Lawnchair have a revenue share agreement.\n\nSearching with %1$s helps support Lawnchair. diff --git a/lawnchair/src/app/lawnchair/qsb/providers/Kagi.kt b/lawnchair/src/app/lawnchair/qsb/providers/Kagi.kt new file mode 100644 index 0000000000..b597baa19a --- /dev/null +++ b/lawnchair/src/app/lawnchair/qsb/providers/Kagi.kt @@ -0,0 +1,16 @@ +package app.lawnchair.qsb.providers + +import app.lawnchair.qsb.ThemingMethod +import com.android.launcher3.R + +data object Kagi : QsbSearchProvider( + id = "kagi", + name = R.string.search_provider_kagi, + icon = R.drawable.ic_kagi, + themedIcon = R.drawable.ic_kagi_tinted, + themingMethod = ThemingMethod.TINT, + packageName = "", + website = "https://kagi.com", + type = QsbSearchProviderType.LOCAL, + sponsored = false, +) diff --git a/lawnchair/src/app/lawnchair/qsb/providers/QsbSearchProvider.kt b/lawnchair/src/app/lawnchair/qsb/providers/QsbSearchProvider.kt index 9ebfb2abe2..906379beb6 100644 --- a/lawnchair/src/app/lawnchair/qsb/providers/QsbSearchProvider.kt +++ b/lawnchair/src/app/lawnchair/qsb/providers/QsbSearchProvider.kt @@ -137,6 +137,7 @@ sealed class QsbSearchProvider( Iceraven, Startpage, Mull, + Kagi, ) /** diff --git a/lawnchair/src/app/lawnchair/search/algorithms/data/Web.kt b/lawnchair/src/app/lawnchair/search/algorithms/data/Web.kt index bf158ec340..fb1b23cd28 100644 --- a/lawnchair/src/app/lawnchair/search/algorithms/data/Web.kt +++ b/lawnchair/src/app/lawnchair/search/algorithms/data/Web.kt @@ -72,6 +72,7 @@ sealed class WebSearchProvider { fun fromString(value: String): WebSearchProvider = when (value) { "google" -> Google "duckduckgo" -> DuckDuckGo + "kagi" -> Kagi else -> StartPage } @@ -81,6 +82,7 @@ sealed class WebSearchProvider { fun values() = listOf( Google, DuckDuckGo, + Kagi, StartPage, ) } @@ -236,6 +238,56 @@ data object DuckDuckGo : WebSearchProvider() { override fun toString() = "duckduckgo" } +/** + * Paid, ad-free search engine. + */ +data object Kagi : WebSearchProvider() { + override var label = R.string.search_provider_kagi + + override val iconRes = R.drawable.ic_kagi + + override val baseUrl = "https://kagi.com/" + + override val service: KagiService by lazy { retrofit.create() } + + override suspend fun getSuggestions(query: String, maxSuggestions: Int): List = withContext(Dispatchers.IO) { + if (query.isBlank() || maxSuggestions <= 0) { + return@withContext emptyList() + } + + try { + var response: Response = service.getSuggestions(query = query) + + if (response.isSuccessful) { + val responseBody = response.body()?.string() ?: return@withContext emptyList() + + val jsonArray = JSONArray(responseBody) + val suggestionsArray = + jsonArray.optJSONArray(1) ?: return@withContext emptyList() + + return@withContext ( + 0 until suggestionsArray.length() + .coerceAtMost(maxSuggestions) + ) + .map { suggestionsArray.getString(it) } + } else { + Log.w( + "KagiSearchProvider", + "Failed to retrieve suggestions: ${response.code()}", + ) + return@withContext emptyList() + } + } catch (e: Exception) { + Log.e("KagiSearchProvider", "Error during suggestion retrieval", e) + return@withContext emptyList() + } + } + + override fun getSearchUrl(query: String) = "https://kagi.com/search?q=$query" + + override fun toString() = "kagi" +} + /** * Provides an interface for getting search suggestions from the web. */ @@ -277,3 +329,13 @@ interface DuckDuckGoService : GenericSearchService { @Query("callback") callback: String = "jsonCallback", ): Response } + +/** + * Web suggestions for [WebSearchProvider.Kagi]. + */ +interface KagiService : GenericSearchService { + @GET("api/autosuggest") + suspend fun getSuggestions( + @Query("q") query: String, + ): Response +}