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