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
+}