diff --git a/data/wikipedia/build.gradle.kts b/data/wikipedia/build.gradle.kts index a842162dd..81f217c4b 100644 --- a/data/wikipedia/build.gradle.kts +++ b/data/wikipedia/build.gradle.kts @@ -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) diff --git a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaApi.kt b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaApi.kt index a1a6864bd..1c8d34ec4 100644 --- a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaApi.kt +++ b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaApi.kt @@ -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, ) +@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() + } } \ No newline at end of file diff --git a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt index be6db982c..4bb9247a5 100644 --- a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt +++ b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt @@ -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
{ - 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> { 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), ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9818a3825..68b078185 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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]