Migrate Wikipedia module to Ktor
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user