Migrate OkHttp to Ktor
This commit is contained in:
@@ -69,20 +69,11 @@ val OpenSourceLicenses = arrayOf(
|
||||
url = "https://square.github.io/okhttp/"
|
||||
),
|
||||
OpenSourceLibrary(
|
||||
name = "Retrofit",
|
||||
description = "A type-safe HTTP client for Android and Java",
|
||||
name = "Ktor Client",
|
||||
description = "A multiplatform asynchronous HTTP client",
|
||||
licenseName = R.string.apache_license_name,
|
||||
licenseText = R.raw.license_apache_2,
|
||||
copyrightNote = "Copyright 2013 Square, Inc.",
|
||||
url = "https://square.github.io/retrofit/"
|
||||
),
|
||||
OpenSourceLibrary(
|
||||
name = "Gson",
|
||||
description = "Gson is a Java library that can be used to convert Java Objects into their JSON representation.",
|
||||
licenseName = R.string.apache_license_name,
|
||||
licenseText = R.raw.license_apache_2,
|
||||
copyrightNote = "Copyright 2008 Google Inc.",
|
||||
url = "https://github.com/google/gson/"
|
||||
url = "https://ktor.io/"
|
||||
),
|
||||
OpenSourceLibrary(
|
||||
name = "commons-suncalc",
|
||||
|
||||
@@ -44,7 +44,7 @@ dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.work)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.bundles.ktor)
|
||||
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:i18n"))
|
||||
|
||||
@@ -2,28 +2,30 @@ package de.mm20.launcher2.currencies
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.Worker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import org.w3c.dom.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class ExchangeRateWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
override fun doWork(): Result {
|
||||
class ExchangeRateWorker(val context: Context, params: WorkerParameters) :
|
||||
CoroutineWorker(context, params) {
|
||||
override suspend fun doWork(): Result {
|
||||
Log.d("MM20", "Updating currency exchange rates")
|
||||
val httpClient = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml")
|
||||
.get()
|
||||
.build()
|
||||
val httpClient = HttpClient()
|
||||
try {
|
||||
val response = httpClient.newCall(request).execute()
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(response.body?.byteStream()
|
||||
?: return Result.retry())
|
||||
val response = httpClient.get {
|
||||
url("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml")
|
||||
}
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(response.bodyAsChannel().toInputStream())
|
||||
val cubes = document.getElementsByTagName("Cube")
|
||||
val values = mutableListOf<Pair<String, Double>>()
|
||||
var timestamp = System.currentTimeMillis()
|
||||
@@ -41,9 +43,9 @@ class ExchangeRateWorker(val context: Context, params: WorkerParameters) : Worke
|
||||
}
|
||||
val currencies = values.map {
|
||||
Currency(
|
||||
symbol = it.first,
|
||||
value = it.second,
|
||||
lastUpdate = timestamp
|
||||
symbol = it.first,
|
||||
value = it.second,
|
||||
lastUpdate = timestamp
|
||||
).toDatabaseEntity()
|
||||
}
|
||||
AppDatabase.getInstance(context).currencyDao().insertAll(currencies)
|
||||
|
||||
@@ -43,7 +43,7 @@ dependencies {
|
||||
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.coil.core)
|
||||
|
||||
implementation(project(":core:base"))
|
||||
|
||||
@@ -16,6 +16,7 @@ fun knownWebsearchByHostname(hostname: String): CustomWebsearchActionBuilder? {
|
||||
"amazon.cn" -> CustomWebsearchActionBuilder(label = "Amazon CN", urlTemplate = "https://www.amazon.cn/s?k=\${1}")
|
||||
"duckduckgo.com" -> CustomWebsearchActionBuilder(label = "DuckDuckGo", urlTemplate = "https://duckduckgo.com/?q=\${1}")
|
||||
"yahoo.com" -> CustomWebsearchActionBuilder(label = "Yahoo", urlTemplate = "https://search.yahoo.com/search?p=\${1}")
|
||||
"ecosia.org" -> CustomWebsearchActionBuilder(label = "Ecosia", urlTemplate = "https://www.ecosia.org/search?q=\${1}")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.database.entities.SearchActionEntity
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.searchactions.actions.ShareAction
|
||||
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.EmailActionBuilder
|
||||
|
||||
@@ -16,6 +16,11 @@ import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.CustomWebsearchActionBuilder
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -24,8 +29,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
@@ -138,16 +141,14 @@ internal class SearchActionServiceImpl(
|
||||
iconSize: Int
|
||||
): CustomWebsearchActionBuilder? {
|
||||
try {
|
||||
val httpClient = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url(openSearchHref)
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
val inputStream = response.body?.byteStream() ?: return null
|
||||
val httpClient = HttpClient()
|
||||
val response = httpClient.get {
|
||||
url(openSearchHref)
|
||||
}
|
||||
val inputStream = response.bodyAsChannel().toInputStream()
|
||||
|
||||
var label: String? = null
|
||||
var urlTemplate: String? = null
|
||||
var icon: String? = null
|
||||
var largestIconSize: Int = 0
|
||||
var largestIcon: String? = null
|
||||
|
||||
|
||||
@@ -14,12 +14,8 @@ import de.mm20.launcher2.weather.GeocoderWeatherProvider
|
||||
import de.mm20.launcher2.weather.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.SerializationException
|
||||
import org.shredzone.commons.suncalc.SunTimes
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
@@ -34,7 +30,7 @@ import kotlin.math.roundToInt
|
||||
internal class MetNoProvider(
|
||||
private val context: Context,
|
||||
private val weatherSettings: WeatherSettings,
|
||||
): GeocoderWeatherProvider(context) {
|
||||
) : GeocoderWeatherProvider(context) {
|
||||
|
||||
private val metNoApi = MetNoApi()
|
||||
|
||||
@@ -43,6 +39,7 @@ internal class MetNoProvider(
|
||||
is WeatherLocation.LatLon -> withContext(Dispatchers.IO) {
|
||||
getWeatherData(location.lat, location.lon, location.name)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.e("MetNoProvider", "Unsupported location type: $location")
|
||||
null
|
||||
@@ -58,7 +55,11 @@ internal class MetNoProvider(
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private suspend fun getWeatherData(lat: Double, lon: Double, locationName: String): List<Forecast>? {
|
||||
private suspend fun getWeatherData(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
locationName: String
|
||||
): List<Forecast>? {
|
||||
val lastUpdate = weatherSettings.lastUpdate.first()
|
||||
try {
|
||||
val forecasts = mutableListOf<Forecast>()
|
||||
@@ -115,7 +116,7 @@ internal class MetNoProvider(
|
||||
)
|
||||
}
|
||||
return forecasts
|
||||
} catch (e: JSONException) {
|
||||
} catch (e: SerializationException) {
|
||||
CrashReporter.logException(e)
|
||||
} catch (e: IOException) {
|
||||
CrashReporter.logException(e)
|
||||
@@ -180,7 +181,6 @@ internal class MetNoProvider(
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun conditionForCode(code: String): String {
|
||||
return context.getString(
|
||||
when (code.substringBefore("_")) {
|
||||
@@ -240,17 +240,22 @@ internal class MetNoProvider(
|
||||
"rainshowersandthunder", "snowandthunder", "snowshowersandthunder",
|
||||
"lightssnowshowersandthunder", "lightsleetandthunder",
|
||||
"lightsnowandthunder" -> Forecast.THUNDERSTORM
|
||||
|
||||
"sleetshowers", "sleet", "lightsleetshowers", "heavysleetshowers", "lightsleet",
|
||||
"heavysleet" -> Forecast.SLEET
|
||||
|
||||
"snowshowers", "snow", "lightsnowshowers", "heavysnowshowers", "lightsnow",
|
||||
"heavysnow" -> Forecast.SNOW
|
||||
|
||||
"heavyrain", "heavyrainshowers" -> Forecast.SHOWERS
|
||||
"heavyrainandthunder", "sleetshowersandthunder", "rainandthunder", "sleetandthunder",
|
||||
"lightrainshowersandthunder", "heavyrainshowersandthunder",
|
||||
"lightssleetshowersandthunder", "lightrainandthunder" -> Forecast.THUNDERSTORM_WITH_RAIN
|
||||
|
||||
"fog" -> Forecast.FOG
|
||||
"heavysleetshowersandthunder",
|
||||
"heavysleetandthunder" -> Forecast.HEAVY_THUNDERSTORM_WITH_RAIN
|
||||
|
||||
"heavysnowshowersandthunder", "heavysnowandthunder" -> Forecast.HEAVY_THUNDERSTORM
|
||||
else -> Forecast.NONE
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.jsoup)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
@@ -6,6 +6,13 @@ import androidx.core.graphics.toColorInt
|
||||
import de.mm20.launcher2.preferences.search.WebsiteSearchSettings
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import de.mm20.launcher2.search.Website
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.client.statement.request
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -13,16 +20,12 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import java.io.IOException
|
||||
import java.io.UncheckedIOException
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
internal class WebsiteRepository(
|
||||
@@ -30,20 +33,20 @@ internal class WebsiteRepository(
|
||||
val settings: WebsiteSearchSettings,
|
||||
) : SearchableRepository<Website> {
|
||||
|
||||
private val httpClient = OkHttpClient
|
||||
.Builder()
|
||||
.connectTimeout(200, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(3000, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(1000, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
private val httpClient by lazy {
|
||||
HttpClient {
|
||||
install(HttpTimeout) {
|
||||
connectTimeoutMillis = 200
|
||||
requestTimeoutMillis = 3000
|
||||
socketTimeoutMillis = 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<Website>> {
|
||||
if (!allowNetwork) return flowOf(persistentListOf())
|
||||
return settings.enabled.transformLatest { enabled ->
|
||||
emit(persistentListOf())
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
}
|
||||
if (!enabled || query.isBlank()) return@transformLatest
|
||||
|
||||
val website = queryWebsite(query)
|
||||
@@ -61,14 +64,11 @@ internal class WebsiteRepository(
|
||||
"$protocol$query"
|
||||
if (!URLUtil.isValidUrl(url)) return@withContext null
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(URL(url))
|
||||
.get()
|
||||
.tag("onlinesearch")
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
val response = httpClient.get {
|
||||
url(url)
|
||||
}
|
||||
url = response.request.url.toString()
|
||||
val body = response.body?.string() ?: return@withContext null
|
||||
val body = response.bodyAsText()
|
||||
val doc = Jsoup.parse(body)
|
||||
var title = doc.select("meta[property=og:title]").attr("content")
|
||||
if (title.isBlank()) title = doc.title()
|
||||
@@ -112,9 +112,9 @@ internal class WebsiteRepository(
|
||||
return result
|
||||
}
|
||||
|
||||
private fun resolveUrl(url: HttpUrl, link: String): String {
|
||||
private fun resolveUrl(url: Url, link: String): String {
|
||||
return try {
|
||||
URL(url.toUrl(), link).toString()
|
||||
URL(URL(url.toString()), link).toString()
|
||||
} catch (e: MalformedURLException) {
|
||||
""
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ The following libraries are commonly used:
|
||||
- **KotlinX serialization** for JSON serialization
|
||||
- **AndroidX Room** to store launcher data in an SQLite database
|
||||
- **AndroidX Datastore** to store additional user preferences
|
||||
- **OkHttp and Retrofit** for HTTP requests
|
||||
- **Ktor** for HTTP requests
|
||||
- Several other **AndroidX** libraries (Work, Lifecycle, AppCompat, …)
|
||||
|
||||
@@ -40,7 +40,6 @@ accompanist = "0.36.0"
|
||||
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"
|
||||
@@ -111,10 +110,6 @@ ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-conte
|
||||
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" }
|
||||
|
||||
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||
|
||||
coil-core = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
|
||||
coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
|
||||
@@ -141,7 +136,6 @@ osmopeninghours = { group = "de.westnordost", name = "osm-opening-hours", versio
|
||||
[bundles]
|
||||
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"]
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.bundles.ktor)
|
||||
|
||||
api(project(":libs:webdav"))
|
||||
implementation(project(":core:i18n"))
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package de.mm20.launcher2.nextcloud
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
data class NcUser(
|
||||
val displayName: String,
|
||||
val username: String
|
||||
val displayName: String,
|
||||
val username: String
|
||||
)
|
||||
|
||||
|
||||
@@ -11,19 +11,27 @@ import androidx.security.crypto.MasterKey
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import de.mm20.launcher2.webdav.WebDavApi
|
||||
import de.mm20.launcher2.webdav.WebDavFile
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.defaultRequest
|
||||
import io.ktor.client.request.basicAuth
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.forms.submitForm
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.parameters
|
||||
import io.ktor.http.path
|
||||
import io.ktor.http.takeFrom
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import okhttp3.internal.EMPTY_REQUEST
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
@@ -31,18 +39,18 @@ class NextcloudApiHelper(val context: Context) {
|
||||
|
||||
|
||||
private val httpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.authenticator(object : Authenticator {
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (response.priorResponse?.priorResponse != null) return null
|
||||
return response.request
|
||||
.newBuilder()
|
||||
.addHeader("Authorization", getAuthorization() ?: return null)
|
||||
.build()
|
||||
HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json.Lenient)
|
||||
}
|
||||
defaultRequest {
|
||||
val user = getUserName()
|
||||
val token = getToken()
|
||||
if (user != null && token != null) {
|
||||
basicAuth(user, token)
|
||||
}
|
||||
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val preferences by lazy {
|
||||
@@ -81,26 +89,28 @@ class NextcloudApiHelper(val context: Context) {
|
||||
* and return the response. Returns null if an error occurs.
|
||||
*/
|
||||
internal suspend fun startLoginFlow(serverUrl: String): LoginFlowResponse? {
|
||||
val request = Request.Builder()
|
||||
.url("$serverUrl/index.php/login/v2")
|
||||
.method("POST", EMPTY_REQUEST)
|
||||
.header("user-agent", context.getString(R.string.app_name))
|
||||
.build()
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
httpClient.post {
|
||||
url {
|
||||
takeFrom(serverUrl)
|
||||
path("index.php", "login", "v2")
|
||||
}
|
||||
header(HttpHeaders.UserAgent, context.getString(R.string.app_name))
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e("NextcloudApiHelper", "HTTP error", e)
|
||||
null
|
||||
}
|
||||
if (response?.code != 200 || response.body == null) {
|
||||
Log.e("NextcloudApiHelper", "Invalid response: ${response?.code} ${response?.message}")
|
||||
if (response?.status != HttpStatusCode.OK) {
|
||||
Log.e(
|
||||
"NextcloudApiHelper",
|
||||
"Invalid response: ${response?.status} ${response?.bodyAsText()}"
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
Json.Lenient.decodeFromStream<LoginFlowResponse>(response.body!!.byteStream())
|
||||
response.body<LoginFlowResponse>()
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("NextcloudApiHelper", "Invalid response body", e)
|
||||
null
|
||||
@@ -108,25 +118,27 @@ class NextcloudApiHelper(val context: Context) {
|
||||
}
|
||||
|
||||
internal suspend fun pollLoginFlow(loginFlow: LoginFlowResponse): LoginPollResponse? {
|
||||
val request = Request.Builder()
|
||||
.url(loginFlow.poll.endpoint)
|
||||
.method("POST", FormBody.Builder().add("token", loginFlow.poll.token).build())
|
||||
.build()
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
}
|
||||
httpClient.submitForm(
|
||||
url = loginFlow.poll.endpoint,
|
||||
formParameters = parameters {
|
||||
append("token", loginFlow.poll.token)
|
||||
}
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.e("NextcloudApiHelper", "HTTP error", e)
|
||||
null
|
||||
}
|
||||
if (response?.code != 200 || response.body == null) {
|
||||
Log.e("NextcloudApiHelper", "Invalid response: ${response?.code} ${response?.message}")
|
||||
if (response?.status != HttpStatusCode.OK) {
|
||||
Log.e(
|
||||
"NextcloudApiHelper",
|
||||
"Invalid response: ${response?.status} ${response?.bodyAsText()}"
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
Json.Lenient.decodeFromStream<LoginPollResponse>(response.body!!.byteStream())
|
||||
response.body<LoginPollResponse>()
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("NextcloudApiHelper", "Invalid response body", e)
|
||||
null
|
||||
@@ -164,28 +176,33 @@ class NextcloudApiHelper(val context: Context) {
|
||||
|
||||
val server = getServer() ?: return null
|
||||
|
||||
val request = Request.Builder()
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.url("$server/ocs/v1.php/cloud/user?format=json")
|
||||
.build()
|
||||
|
||||
val response = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
val response = try {
|
||||
httpClient.get {
|
||||
url {
|
||||
takeFrom(server)
|
||||
path("ocs", "v1.php", "cloud", "user")
|
||||
parameter("format", "json")
|
||||
}
|
||||
header("OCS-APIRequest", "true")
|
||||
}
|
||||
}.getOrNull() ?: return getUserName()
|
||||
} catch (e: Exception) {
|
||||
Log.e("NextcloudApiHelper", "HTTP error", e)
|
||||
return getUserName()
|
||||
}
|
||||
|
||||
if (response.code != 200) {
|
||||
if (response.status != HttpStatusCode.OK) {
|
||||
logout()
|
||||
return null
|
||||
}
|
||||
val body = response.body ?: return getUserName()
|
||||
val body = try {
|
||||
response.body<UserReponse>()
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("NextcloudApiHelper", "Invalid response body", e)
|
||||
return getUserName()
|
||||
}
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val json = JSONObject(body.string())
|
||||
val name = json.optJSONObject("ocs")
|
||||
?.optJSONObject("data")
|
||||
?.optString("display-name")
|
||||
val name = body.ocs.data.displayName
|
||||
|
||||
preferences.edit {
|
||||
putString("displayname", name)
|
||||
@@ -196,10 +213,6 @@ class NextcloudApiHelper(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthorization(): String? {
|
||||
return Credentials.basic(getUserName() ?: return null, getToken() ?: return null)
|
||||
}
|
||||
|
||||
fun getServer(): String? {
|
||||
return preferences.getString("server", null)
|
||||
}
|
||||
@@ -225,15 +238,15 @@ class NextcloudApiHelper(val context: Context) {
|
||||
val username = getUserName()
|
||||
val token = getToken()
|
||||
if (server == null || username == null || token == null) return
|
||||
val request = Request.Builder()
|
||||
.addHeader("OCS-APIREQUEST", "true")
|
||||
.delete()
|
||||
.url("$server/ocs/v2.php/core/apppassword")
|
||||
.build()
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val response = httpClient.newCall(request).execute()
|
||||
response
|
||||
httpClient.delete {
|
||||
url {
|
||||
takeFrom(server)
|
||||
path("ocs", "v2.php", "core", "apppassword")
|
||||
}
|
||||
header("OCS-APIREQUEST", "true")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e("NextcloudApiHelper", "Error during Nextcloud logout", e)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.mm20.launcher2.nextcloud
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class UserReponse(
|
||||
val ocs: UserReponseOcs
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
internal data class UserReponseOcs(
|
||||
val data: UserReponseOcsData
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class UserReponseOcsData(
|
||||
@SerialName("display-name") val displayName: String?,
|
||||
)
|
||||
@@ -54,11 +54,12 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.bundles.ktor)
|
||||
|
||||
api(project(":libs:webdav"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":core:base"))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.mm20.launcher2.owncloud
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class UserReponse(
|
||||
val ocs: UserReponseOcs
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
internal data class UserReponseOcs(
|
||||
val data: UserReponseOcsData
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class UserReponseOcsData(
|
||||
@SerialName("display-name") val displayName: String?,
|
||||
)
|
||||
@@ -8,13 +8,24 @@ import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import de.mm20.launcher2.webdav.WebDavApi
|
||||
import de.mm20.launcher2.webdav.WebDavFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.*
|
||||
import org.json.JSONObject
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.defaultRequest
|
||||
import io.ktor.client.request.basicAuth
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.appendPathSegments
|
||||
import io.ktor.http.path
|
||||
import io.ktor.http.takeFrom
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.SerializationException
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
@@ -22,18 +33,18 @@ class OwncloudClient(val context: Context) {
|
||||
|
||||
|
||||
private val httpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.authenticator(object : Authenticator {
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (response.priorResponse?.priorResponse != null) return null
|
||||
return response.request
|
||||
.newBuilder()
|
||||
.addHeader("Authorization", getAuthorization() ?: return null)
|
||||
.build()
|
||||
}
|
||||
|
||||
})
|
||||
.build()
|
||||
HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json.Lenient)
|
||||
}
|
||||
defaultRequest {
|
||||
val user = getUserName()
|
||||
val token = getToken()
|
||||
if (user != null && token != null) {
|
||||
basicAuth(user, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val preferences by lazy {
|
||||
@@ -42,7 +53,8 @@ class OwncloudClient(val context: Context) {
|
||||
|
||||
private fun createPreferences(catchErrors: Boolean = true): SharedPreferences {
|
||||
try {
|
||||
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||
val masterKey =
|
||||
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||
return EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"owncloud",
|
||||
@@ -71,34 +83,42 @@ class OwncloudClient(val context: Context) {
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://$url"
|
||||
}
|
||||
val request = Request.Builder()
|
||||
.url("$url/remote.php/webdav")
|
||||
.build()
|
||||
val response = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
}
|
||||
}.getOrNull() ?: return false
|
||||
return response.code == 200 || response.code == 401
|
||||
}
|
||||
|
||||
internal suspend fun checkOwncloudCredentials(server: String, username: String, password: String): Boolean {
|
||||
val request = Request.Builder()
|
||||
.addHeader("authorization", Credentials.basic(username, password))
|
||||
.url("$server/ocs/v1.php/cloud/user?format=json")
|
||||
.build()
|
||||
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
httpClient.get {
|
||||
url {
|
||||
takeFrom(url)
|
||||
appendPathSegments("remote.php", "webdav")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
|
||||
return response.status == HttpStatusCode.OK || response.status == HttpStatusCode.Unauthorized
|
||||
}
|
||||
|
||||
internal suspend fun checkOwncloudCredentials(
|
||||
server: String,
|
||||
username: String,
|
||||
password: String
|
||||
): Boolean {
|
||||
val response = try {
|
||||
httpClient.get {
|
||||
url {
|
||||
takeFrom(server)
|
||||
path("ocs", "v1.php", "cloud", "user")
|
||||
parameter("format", "json")
|
||||
}
|
||||
basicAuth(username, password)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e("OwncloudClient", "HTTP error", e)
|
||||
return false
|
||||
}
|
||||
|
||||
if (response.code != 200) {
|
||||
Log.e("OwncloudClient", "HTTP error: ${response.code}")
|
||||
if (response.status != HttpStatusCode.OK) {
|
||||
Log.e("OwncloudClient", "HTTP error: ${response.status}")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -118,8 +138,8 @@ class OwncloudClient(val context: Context) {
|
||||
val displayName = getDisplayName() ?: return null
|
||||
|
||||
return OcUser(
|
||||
displayName,
|
||||
username
|
||||
displayName,
|
||||
username
|
||||
)
|
||||
}
|
||||
|
||||
@@ -134,36 +154,31 @@ class OwncloudClient(val context: Context) {
|
||||
}
|
||||
|
||||
val server = getServer() ?: return null
|
||||
|
||||
val request = Request.Builder()
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.url("$server/ocs/v1.php/cloud/user?format=json")
|
||||
.build()
|
||||
|
||||
val response = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
val response = try {
|
||||
httpClient.get {
|
||||
url {
|
||||
takeFrom(server)
|
||||
path("ocs", "v1.php", "cloud", "user")
|
||||
parameter("format", "json")
|
||||
}
|
||||
header("OCS-APIRequest", "true")
|
||||
}
|
||||
}.getOrNull() ?: return getUserName()
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
return getUserName()
|
||||
}
|
||||
|
||||
if (response.code != 200) {
|
||||
if (response.status != HttpStatusCode.OK) {
|
||||
logout()
|
||||
return null
|
||||
}
|
||||
val body = response.body ?: return getUserName()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val json = JSONObject(body.string())
|
||||
|
||||
return@withContext json.optJSONObject("ocs")
|
||||
?.optJSONObject("data")
|
||||
?.optString("display-name")
|
||||
?: getUserName()
|
||||
val body = try {
|
||||
response.body<UserReponse>()
|
||||
} catch (e: SerializationException) {
|
||||
CrashReporter.logException(e)
|
||||
return getUserName()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAuthorization(): String? {
|
||||
return Credentials.basic(getUserName() ?: return null, getToken() ?: return null)
|
||||
return body.ocs.data.displayName ?: getUserName()
|
||||
}
|
||||
|
||||
fun getServer(): String? {
|
||||
|
||||
@@ -42,7 +42,7 @@ dependencies {
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.bundles.ktor)
|
||||
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:ktx"))
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
package de.mm20.launcher2.webdav
|
||||
|
||||
import com.balsikandar.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.castToOrNull
|
||||
import de.mm20.launcher2.ktx.decodeUrl
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.appendPathSegments
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.takeFrom
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.w3c.dom.Element
|
||||
import org.xml.sax.SAXException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.Exception
|
||||
import java.net.URLDecoder
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
|
||||
object WebDavApi {
|
||||
suspend fun search(webDavUrl: String, username: String, query: String, client: OkHttpClient): List<WebDavFile> {
|
||||
suspend fun search(
|
||||
webDavUrl: String,
|
||||
username: String,
|
||||
query: String,
|
||||
client: HttpClient
|
||||
): List<WebDavFile> {
|
||||
val requestBody = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||
@@ -52,54 +57,55 @@ object WebDavApi {
|
||||
</d:basicsearch>
|
||||
</d:searchrequest>
|
||||
""".trimIndent()
|
||||
val request = Request.Builder()
|
||||
.url(webDavUrl)
|
||||
.method("SEARCH", requestBody.toRequestBody("text/xml".toMediaType()))
|
||||
.build()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val results = mutableListOf<WebDavFile>()
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(response.body?.byteStream()
|
||||
?: return@withContext emptyList<WebDavFile>())
|
||||
val response = client.request {
|
||||
method = HttpMethod("SEARCH")
|
||||
url(webDavUrl)
|
||||
setBody(requestBody)
|
||||
contentType(ContentType.Text.Xml)
|
||||
}
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(response.bodyAsChannel().toInputStream())
|
||||
val responses = document.getElementsByTagName("d:response")
|
||||
for (i in 0 until responses.length) {
|
||||
val res = responses.item(i) as? Element ?: continue
|
||||
val url = res.getElementsByTagName("d:href")
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.takeIf { it.isNotEmpty() } ?: continue
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.takeIf { it.isNotEmpty() } ?: continue
|
||||
val fileId = res.getElementsByTagName("oc:fileid")
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.toLongOrNull() ?: continue
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.toLongOrNull() ?: continue
|
||||
|
||||
val displayName = res.getElementsByTagName("d:displayname")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: url.trimEnd('/').substringAfterLast("/").decodeUrl("utf8")
|
||||
?: continue
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: url.trimEnd('/').substringAfterLast("/").decodeUrl("utf8")
|
||||
?: continue
|
||||
|
||||
val isDirectory = res.getElementsByTagName("d:resourcetype")
|
||||
.takeIf { it.length > 0 }
|
||||
?.item(0)?.childNodes?.length == 1
|
||||
.takeIf { it.length > 0 }
|
||||
?.item(0)?.childNodes?.length == 1
|
||||
val mimeType = res.getElementsByTagName("d:getcontenttype")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.takeIf { it.isNotEmpty() }
|
||||
?: if (isDirectory) "inode/directory" else "application/octet-stream"
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.takeIf { it.isNotEmpty() }
|
||||
?: if (isDirectory) "inode/directory" else "application/octet-stream"
|
||||
val size = res.getElementsByTagName("oc:size")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.toLongOrNull()
|
||||
?: 0L
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.toLongOrNull()
|
||||
?: 0L
|
||||
val owner = res.getElementsByTagName("oc:owner-display-name")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
|
||||
results += WebDavFile(
|
||||
name = displayName,
|
||||
id = fileId,
|
||||
isDirectory = isDirectory,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
owner = owner,
|
||||
url = url
|
||||
name = displayName,
|
||||
id = fileId,
|
||||
isDirectory = isDirectory,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
owner = owner,
|
||||
url = url
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -131,56 +137,65 @@ object WebDavApi {
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
suspend fun searchReport(webDavUrl: String, username: String, query: String, client: OkHttpClient): List<WebDavFile> {
|
||||
suspend fun searchReport(
|
||||
webDavUrl: String,
|
||||
username: String,
|
||||
query: String,
|
||||
client: HttpClient
|
||||
): List<WebDavFile> {
|
||||
val requestBody = getSearchRequestBody(query)
|
||||
val request = Request.Builder()
|
||||
.url("${webDavUrl}files/$username")
|
||||
.method("REPORT", requestBody.toRequestBody())
|
||||
.build()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val results = mutableListOf<WebDavFile>()
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(response.body?.byteStream()
|
||||
?: return@withContext emptyList<WebDavFile>())
|
||||
val response = client.request {
|
||||
method = HttpMethod("REPORT")
|
||||
url {
|
||||
takeFrom(webDavUrl)
|
||||
appendPathSegments("files", username)
|
||||
}
|
||||
setBody(requestBody)
|
||||
contentType(ContentType.Text.Xml)
|
||||
}
|
||||
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(response.bodyAsChannel().toInputStream())
|
||||
val responses = document.getElementsByTagName("d:response")
|
||||
for (i in 0 until responses.length) {
|
||||
val res = responses.item(i) as? Element ?: continue
|
||||
val url = res.getElementsByTagName("d:href")
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.takeIf { it.isNotEmpty() } ?: continue
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.takeIf { it.isNotEmpty() } ?: continue
|
||||
val fileId = res.getElementsByTagName("oc:fileid")
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.toLongOrNull() ?: continue
|
||||
.takeIf { it.length > 0 }?.item(0)
|
||||
?.textContent?.toLongOrNull() ?: continue
|
||||
|
||||
val displayName = res.getElementsByTagName("d:displayname")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: url.trimEnd('/').substringAfterLast("/").decodeUrl("utf8")
|
||||
?: continue
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: url.trimEnd('/').substringAfterLast("/").decodeUrl("utf8")
|
||||
?: continue
|
||||
|
||||
val isDirectory = res.getElementsByTagName("d:resourcetype")
|
||||
.takeIf { it.length > 0 }
|
||||
?.item(0)?.childNodes?.length == 1
|
||||
.takeIf { it.length > 0 }
|
||||
?.item(0)?.childNodes?.length == 1
|
||||
val mimeType = res.getElementsByTagName("d:getcontenttype")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.takeIf { it.isNotEmpty() }
|
||||
?: if (isDirectory) "inode/directory" else "application/octet-stream"
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.takeIf { it.isNotEmpty() }
|
||||
?: if (isDirectory) "inode/directory" else "application/octet-stream"
|
||||
val size = res.getElementsByTagName("oc:size")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.toLongOrNull()
|
||||
?: 0L
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent?.toLongOrNull()
|
||||
?: 0L
|
||||
val owner = res.getElementsByTagName("oc:owner-display-name")
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
.takeIf { it.length > 0 }?.item(0)?.textContent
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
|
||||
results += WebDavFile(
|
||||
name = displayName,
|
||||
id = fileId,
|
||||
isDirectory = isDirectory,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
owner = owner,
|
||||
url = url
|
||||
name = displayName,
|
||||
id = fileId,
|
||||
isDirectory = isDirectory,
|
||||
mimeType = mimeType,
|
||||
size = size,
|
||||
owner = owner,
|
||||
url = url
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -47,7 +47,6 @@ dependencies {
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.coil.core)
|
||||
|
||||
implementation(project(":data:calculator"))
|
||||
|
||||
Reference in New Issue
Block a user