Migrate Wikipedia module to Ktor

This commit is contained in:
MM20
2025-09-06 21:42:15 +02:00
parent c2f160f952
commit 97d2668ddc
4 changed files with 87 additions and 65 deletions

View File

@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.serialization)
}
android {
@@ -45,8 +46,7 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.okhttp)
implementation(libs.bundles.retrofit)
implementation(libs.bundles.ktor)
implementation(libs.koin.android)

View File

@@ -1,16 +1,31 @@
package de.mm20.launcher2.wikipedia
import retrofit2.http.GET
import retrofit2.http.Query
import android.content.Context
import de.mm20.launcher2.serialization.Json
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.UserAgent
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.http.path
import io.ktor.http.takeFrom
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable
@Serializable
data class WikipediaSearchResult(
val query: WikipediaSearchResultQuery?,
)
@Serializable
data class WikipediaSearchResultQuery(
val pages: Map<String, WikipediaSearchResultQueryPage>,
)
@Serializable
data class WikipediaSearchResultQueryPage(
val pageid: Long,
val title: String,
@@ -20,11 +35,56 @@ data class WikipediaSearchResultQueryPage(
val canonicalurl: String,
)
@Serializable
data class WikipediaSearchResultQueryPageThumnail(
val source: String
)
interface WikipediaApi {
@GET("w/api.php?action=query&generator=search&redirects=true&gsrlimit=1&prop=extracts|info|pageimages&exchars=500&exintro=true&inprop=url&format=json")
suspend fun search(@Query("gsrsearch") query: String, @Query("pithumbsize") thumbnailSize: Int): WikipediaSearchResult
internal class WikipediaApi(
private val context: Context,
var baseUrl: String?,
) {
private val appVersion =
context.packageManager.getPackageInfo(context.packageName, 0).versionName
val httpClient by lazy {
HttpClient {
install(ContentNegotiation) {
json(Json.Lenient)
}
install(UserAgent) {
agent = "${context.packageName}/v$appVersion"
}
install(HttpTimeout) {
connectTimeoutMillis = 200
requestTimeoutMillis = 3000
socketTimeoutMillis = 1000
}
defaultRequest {
url(baseUrl)
}
}
}
suspend fun search(baseUrl: String, query: String, thumbnailSize: Int): WikipediaSearchResult {
return httpClient.get {
url {
takeFrom(baseUrl)
if (pathSegments.isEmpty()) {
path("w", "api.php")
}
parameter("action", "query")
parameter("generator", "search")
parameter("redirects", "true")
parameter("gsrlimit", "1")
parameter("prop", "extracts|info|pageimages")
parameter("exchars", "500")
parameter("exintro", "true")
parameter("inprop", "url")
parameter("format", "json")
parameter("gsrsearch", query)
parameter("pithumbsize", thumbnailSize)
}
}.body()
}
}

View File

@@ -7,12 +7,9 @@ import de.mm20.launcher2.search.Article
import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.flowOf
internal class WikipediaRepository(
@@ -20,59 +17,20 @@ internal class WikipediaRepository(
private val settings: WikipediaSearchSettings,
) : SearchableRepository<Article> {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val appVersion by lazy {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
}
private val httpClient = OkHttpClient
.Builder()
.connectTimeout(200, TimeUnit.MILLISECONDS)
.readTimeout(3000, TimeUnit.MILLISECONDS)
.writeTimeout(1000, TimeUnit.MILLISECONDS)
.addInterceptor {
it.proceed(it.request().newBuilder().addHeader("user-agent", "${context.packageName}/v$appVersion").build())
}
.build()
private lateinit var retrofit: Retrofit
init {
scope.launch {
settings.customUrl
.distinctUntilChanged()
.collectLatest {
try { retrofit = Retrofit.Builder()
.client(httpClient)
.baseUrl(it.takeIf { !it.isNullOrBlank() }
?: context.getString(R.string.wikipedia_url))
.addConverterFactory(GsonConverterFactory.create())
.build()
wikipediaService = retrofit.create(WikipediaApi::class.java)
} catch (e: IllegalArgumentException) {
CrashReporter.logException(e)
}
}
}
}
private lateinit var wikipediaService: WikipediaApi
private val wikipediaApi = WikipediaApi(context, null)
override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<Wikipedia>> {
if (query.length < 4 || !allowNetwork) return flowOf(persistentListOf())
return settings.enabled.transformLatest {
return combineTransform(settings.enabled, settings.customUrl) { enabled, url ->
emit(persistentListOf())
withContext(Dispatchers.IO) {
httpClient.dispatcher.cancelAll()
}
if (!it || !::wikipediaService.isInitialized) return@transformLatest
if (query.isBlank()) return@transformLatest
if (query.isBlank()) return@combineTransform
val results = queryWikipedia(query)
val baseUrl =
url.takeIf { !it.isNullOrBlank() } ?: context.getString(R.string.wikipedia_url)
val results = queryWikipedia(baseUrl, query)
if (results != null) {
emit(persistentListOf(results))
}
@@ -80,14 +38,11 @@ internal class WikipediaRepository(
}
private suspend fun queryWikipedia(query: String): Wikipedia? {
val wikipediaService = wikipediaService
val wikipediaUrl = retrofit.baseUrl().toString()
private suspend fun queryWikipedia(baseUrl: String, query: String): Wikipedia? {
val result = try {
val imageWidth = context.resources.displayMetrics.widthPixels / 2
wikipediaService.search(query, imageWidth)
wikipediaApi.search(baseUrl, query, imageWidth)
} catch (e: Exception) {
CrashReporter.logException(e)
return null
@@ -103,7 +58,7 @@ internal class WikipediaRepository(
text = page.extract,
imageUrl = image,
sourceUrl = page.fullurl,
wikipediaUrl = wikipediaUrl,
wikipediaUrl = baseUrl,
sourceName = context.getString(R.string.wikipedia_source),
)
}

View File

@@ -41,6 +41,7 @@ haze = "1.6.10"
coil = "2.7.0"
koin = "4.1.1"
retrofit = "2.11.0"
ktor = "3.2.3"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
@@ -104,6 +105,11 @@ androidx-securitycrypto = { group = "androidx.security", name = "security-crypto
androidx-datastore = { group = "androidx.datastore", name = "datastore", version = "1.2.0-alpha02" }
androidx-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "androidx-emojipicker" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version = "2.9.0" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version = "4.12.0" }
@@ -136,6 +142,7 @@ osmopeninghours = { group = "de.westnordost", name = "osm-opening-hours", versio
kotlin = ["kotlin-stdlib", "kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-collections-immutable", "kotlinx-serialization-json"]
androidx-lifecycle = ["androidx-lifecycle-viewmodel", "androidx-lifecycle-common", "androidx-lifecycle-runtime", "androidx-lifecycle-viewmodelcompose", "androidx-lifecycle-runtimecompose"]
retrofit = ["retrofit-core", "retrofit-gson"]
ktor = ["ktor-client-core", "ktor-client-okhttp", "ktor-serialization-kotlinx-json", "ktor-client-content-negotiation"]
tests = ["junit"]
[plugins]