parent
4485b59ade
commit
53f305acd9
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.plugin.serialization)
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
@ -53,5 +54,6 @@ dependencies {
|
||||
|
||||
api(project(":libs:webdav"))
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":core:base"))
|
||||
|
||||
}
|
||||
@ -1,14 +1,12 @@
|
||||
package de.mm20.launcher2.nextcloud
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.util.Log
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
@ -40,6 +38,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -118,8 +117,9 @@ class LoginActivity : AppCompatActivity() {
|
||||
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
|
||||
url = "https://$url"
|
||||
}
|
||||
if (nextcloudClient.checkNextcloudInstallation(url)) {
|
||||
openLoginPage(url)
|
||||
val flow = nextcloudClient.startLoginFlow(url)
|
||||
if (flow != null) {
|
||||
openLoginPage(flow)
|
||||
} else {
|
||||
error = getString(R.string.nextcloud_server_invalid_url)
|
||||
}
|
||||
@ -135,49 +135,24 @@ class LoginActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLoginPage(url: String) {
|
||||
val webView = WebView(this)
|
||||
webView.settings.userAgentString = getString(R.string.app_name)
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
if (request?.url?.scheme == "nc") {
|
||||
val path = request.url?.path?.trim('/') ?: run {
|
||||
setResult(0)
|
||||
finish()
|
||||
return false
|
||||
}
|
||||
val segments = path.split('&')
|
||||
var username: String? = null
|
||||
var token: String? = null
|
||||
var server: String? = null
|
||||
for (segment in segments) {
|
||||
when {
|
||||
segment.startsWith("server") -> server = segment.substringAfter(":")
|
||||
segment.startsWith("user") -> username = segment.substringAfter(":")
|
||||
segment.startsWith("password") -> token = segment.substringAfter(":")
|
||||
}
|
||||
}
|
||||
if (username != null && server != null && token != null) {
|
||||
nextcloudClient.setServer(server, username, token)
|
||||
}
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
webView.loadUrl(request?.url?.toString() ?: "")
|
||||
return false
|
||||
private var currentLoginFlow: LoginFlowResponse? = null
|
||||
private fun openLoginPage(flow: LoginFlowResponse) {
|
||||
currentLoginFlow = flow
|
||||
val customTabIntent = CustomTabsIntent.Builder().build()
|
||||
customTabIntent.launchUrl(this, flow.login.toUri())
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val flow = currentLoginFlow ?: return
|
||||
lifecycleScope.launch {
|
||||
val result = nextcloudClient.pollLoginFlow(flow)
|
||||
if (result != null) {
|
||||
nextcloudClient.setServer(result.server, result.loginName, result.appPassword)
|
||||
currentLoginFlow = null
|
||||
finish()
|
||||
}
|
||||
}
|
||||
webView.settings.javaScriptEnabled = true
|
||||
setContentView(webView)
|
||||
val headers = mapOf(
|
||||
"OCS-APIREQUEST" to "true"
|
||||
)
|
||||
webView.loadUrl("$url/index.php/login/flow", headers)
|
||||
|
||||
}
|
||||
|
||||
private val nextcloudLight: ColorScheme
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package de.mm20.launcher2.nextcloud
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class LoginFlowResponse(
|
||||
val poll: LoginFlowResponsePoll,
|
||||
val login: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class LoginFlowResponsePoll(
|
||||
val token: String,
|
||||
val endpoint: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class LoginPollResponse(
|
||||
val server: String,
|
||||
val loginName: String,
|
||||
val appPassword: String,
|
||||
)
|
||||
@ -4,14 +4,25 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
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 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
|
||||
@ -65,20 +76,61 @@ class NextcloudApiHelper(val context: Context) {
|
||||
}
|
||||
|
||||
|
||||
suspend fun checkNextcloudInstallation(url: String): Boolean {
|
||||
var url = url
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://$url"
|
||||
}
|
||||
/**
|
||||
* Perform a POST request to the /index.php/login/v2 endpoint of the given Nextcloud server
|
||||
* and return the response. Returns null if an error occurs.
|
||||
*/
|
||||
internal suspend fun startLoginFlow(serverUrl: String): LoginFlowResponse? {
|
||||
val request = Request.Builder()
|
||||
.url("$url/remote.php/dav")
|
||||
.url("$serverUrl/index.php/login/v2")
|
||||
.method("POST", EMPTY_REQUEST)
|
||||
.header("user-agent", context.getString(R.string.app_name))
|
||||
.build()
|
||||
val response = runCatching {
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(request).execute()
|
||||
}
|
||||
}.getOrNull() ?: return false
|
||||
return response.code == 200 || response.code == 401
|
||||
} 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}")
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
Json.Lenient.decodeFromStream<LoginFlowResponse>(response.body!!.byteStream())
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("NextcloudApiHelper", "Invalid response body", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
} 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}")
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
Json.Lenient.decodeFromStream<LoginPollResponse>(response.body!!.byteStream())
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("NextcloudApiHelper", "Invalid response body", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLoggedInUser(): NcUser? {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user