Initial commit

This commit is contained in:
MM20
2021-09-18 23:37:52 +02:00
commit 749e4e3073
938 changed files with 50475 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
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 androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.activity_nextcloud_login.*
import kotlinx.coroutines.*
class LoginActivity : AppCompatActivity() {
private val nextcloudClient = NextcloudApiHelper(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_nextcloud_login)
nextButton.setOnClickListener {
serverUrlInputLayout.error = null
lifecycleScope.launch {
var url = serverUrlInput.text.toString()
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
url = "https://$url"
}
if (url.isBlank()) {
serverUrlInputLayout.error = getString(R.string.next_cloud_server_url_empty)
return@launch
}
if (nextcloudClient.checkNextcloudInstallation(url)) {
openLoginPage(url)
} else {
serverUrlInputLayout.error = getString(R.string.next_cloud_server_invalid_url)
}
}
}
}
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
}
}
webView.settings.javaScriptEnabled = true
setContentView(webView)
val headers = mapOf(
"OCS-APIREQUEST" to "true"
)
webView.loadUrl("$url/index.php/login/flow", headers)
}
}

View File

@@ -0,0 +1,6 @@
package de.mm20.launcher2.nextcloud
data class NcUser(
val displayName: String,
val username: String
)

View File

@@ -0,0 +1,217 @@
package de.mm20.launcher2.nextcloud
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
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 java.io.File
import java.io.IOException
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()
}
})
.build()
}
private val preferences by lazy {
createPreferences()
}
private fun createPreferences(catchErrors: Boolean = true): SharedPreferences {
try {
val masterKey =
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
return EncryptedSharedPreferences.create(
context,
"nextcloud",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
).also {
if (!it.getBoolean("encrypted", false)) {
val legacyPrefs =
context.getSharedPreferences("nextcloud", Context.MODE_PRIVATE)
val keys = arrayOf("server", "username", "token", "displayname")
it.edit {
for (k in keys) {
putString(k, legacyPrefs.getString(k, null))
}
putBoolean("encrypted", true)
}
legacyPrefs.edit {
for (k in keys) {
putString(k, null)
}
}
}
}
} catch (e: IOException) {
if (!catchErrors) throw e
File(context.filesDir, "../shared_prefs/nextcloud.xml").delete()
return createPreferences(false)
}
}
fun login(activity: Activity) {
activity.startActivity(Intent(context, LoginActivity::class.java))
}
suspend fun checkNextcloudInstallation(url: String): Boolean {
var url = url
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://$url"
}
val request = Request.Builder()
.url("$url/remote.php/dav")
.build()
val response = runCatching {
withContext(Dispatchers.IO) {
httpClient.newCall(request).execute()
}
}.getOrNull() ?: return false
return response.code == 200 || response.code == 401
}
suspend fun getLoggedInUser(): NcUser? {
val server = getServer()
val username = getUserName()
val token = getToken()
if (server == null || username == null || token == null) {
return null
}
val displayName = getDisplayName() ?: return null
return NcUser(
displayName,
username
)
}
/**
* Returns the user's display name or user name if the user is logged in and their token has
* not been revoked,
* returns null if they are not logged in.
*/
private suspend fun getDisplayName(): String? {
val displayname = preferences.getString("displayname", null)
if (displayname != null) {
return displayname
}
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()
}
}.getOrNull() ?: return getUserName()
if (response.code != 200) {
logout()
return null
}
val body = response.body ?: return getUserName()
return withContext(Dispatchers.IO) {
val json = JSONObject(body.string())
val name = json.optJSONObject("ocs")
?.optJSONObject("data")
?.optString("display-name")
preferences.edit {
putString("displayname", name)
}
return@withContext name
?: getUserName()
}
}
private fun getAuthorization(): String? {
return Credentials.basic(getUserName() ?: return null, getToken() ?: return null)
}
fun getServer(): String? {
return preferences.getString("server", null)
}
fun getUserName(): String? {
return preferences.getString("username", null)
}
private fun getToken(): String? {
return preferences.getString("token", null)
}
internal fun setServer(server: String, username: String, token: String) {
preferences.edit {
putString("server", server)
putString("username", username)
putString("token", token)
}
}
suspend fun logout() {
val server = getServer()
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) {
val response = httpClient.newCall(request).execute()
response
}
preferences.edit {
putString("server", null)
putString("username", null)
putString("token", null)
putString("displayname", null)
}
}
val files by lazy {
FilesApi()
}
inner class FilesApi internal constructor() {
suspend fun search(query: String): List<WebDavFile> {
val server = getServer() ?: return emptyList()
val username = getUserName() ?: return emptyList()
return WebDavApi.search("$server/remote.php/dav/", username, query, httpClient)
}
}
}