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
+17
View File
@@ -0,0 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.owncloud">
<application>
<activity
android:name=".LoginActivity"
android:label="@string/preference_category_services_nextcloud"
android:taskAffinity="de.mm20.launcher2.nextcloud"
android:parentActivityName=".activity.SettingsActivity"
android:theme="@style/OwncloudLoginTheme" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.mm20.launcher2.activity.SettingsActivity" />
</activity>
</application>
</manifest>
@@ -0,0 +1,62 @@
package de.mm20.launcher2.owncloud
import android.app.Activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.activity_owncloud_login.*
import kotlinx.android.synthetic.main.activity_owncloud_login_username_password.*
import kotlinx.coroutines.*
class LoginActivity : AppCompatActivity() {
private val owncloudClient = OwncloudClient(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_owncloud_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 (owncloudClient.checkOwncloudInstallation(url)) {
openLoginPage(url)
} else {
serverUrlInputLayout.error = getString(R.string.owncloud_server_invalid_url)
}
}
}
}
private fun openLoginPage(url: String) {
setContentView(R.layout.activity_owncloud_login_username_password)
loginButton.setOnClickListener {
val username = username.text.toString()
val password = password.text.toString()
if (username.isEmpty()) {
usernameInputLayout.error = getString(R.string.owncloud_username_empty)
}
if (password.isEmpty()) {
passwordInputLayout.error = getString(R.string.owncloud_password_empty)
}
if(username.isEmpty() || password.isEmpty()) {
return@setOnClickListener
}
lifecycleScope.launch {
if (owncloudClient.tryLogin(url, username, password)) {
setResult(Activity.RESULT_OK)
finish()
} else {
passwordInputLayout.error = getString(R.string.owncloud_login_failed)
}
}
}
}
}
@@ -0,0 +1,6 @@
package de.mm20.launcher2.owncloud
data class OcUser(
val displayName: String,
val username: String
)
@@ -0,0 +1,197 @@
package de.mm20.launcher2.owncloud
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 androidx.security.crypto.MasterKeys
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 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()
}
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,
"owncloud",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (e: IOException) {
if (!catchErrors) throw e
File(context.filesDir, "../shared_prefs/owncloud.xml").delete()
return createPreferences(false)
}
}
fun login(activity: Activity, requestCode: Int) {
activity.startActivityForResult(Intent(context, LoginActivity::class.java), requestCode)
}
suspend fun checkOwncloudInstallation(url: String): Boolean {
var url = url
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
}
suspend fun getLoggedInUser(): OcUser? {
val server = getServer()
val username = getUserName()
val token = getToken()
if (server == null || username == null || token == null) {
return null
}
val displayName = getDisplayName() ?: return null
return OcUser(
displayName,
username
)
}
/**
* Returns the user's display name or user name if the user is logged in
* returns null if they are not logged in.
*/
private suspend fun getDisplayName(): String? {
if (preferences.getString("displayname", null) != null) {
return preferences.getString("displayname", null)
}
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())
return@withContext json.optJSONObject("ocs")
?.optJSONObject("data")
?.optString("display-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)
}
fun getUserDisplayName(): String? {
return preferences.getString("displayname", getUserName())
}
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)
}
}
fun logout() {
preferences.edit {
putString("server", null)
putString("username", null)
putString("token", null)
putString("displayname", null)
}
}
suspend fun tryLogin(url: String, username: String, pw: String): Boolean {
setServer(url, username, pw)
val displayName = getDisplayName()
preferences.edit {
putString("displayname", displayName)
}
return displayName != null
}
val files by lazy {
FilesApi()
}
inner class FilesApi internal constructor() {
suspend fun query(query: String): List<WebDavFile> {
val server = getServer() ?: return emptyList()
val username = getUserName() ?: return emptyList()
return WebDavApi.searchReport("$server/remote.php/dav/", username, query, httpClient)
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/serverUrlView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="32dp"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_owncloud_logo" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/serverUrlInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:hint="@string/owncloud_server_url"
app:helperTextEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/serverUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/nextButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_flow_next" />
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/serverUrlView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="32dp"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_owncloud_logo" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/usernameInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/owncloud_username"
app:helperTextEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="username"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"
app:helperText="@string/owncloud_login_2fa_hint"
android:hint="@string/owncloud_password">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:inputType="textWebPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_flow_login" />
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:padding="16dp"
android:textAppearance="?attr/textAppearanceSubtitle1" />
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="OwncloudLoginTheme" parent="Theme.MaterialComponents.NoActionBar">
<item name="colorAccent">#6d8fc0</item>
<item name="colorPrimary">#6d8fc0</item>
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
</style>
</resources>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="OwncloudLoginTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="colorAccent">#1d2d44</item>
<item name="colorPrimary">#1d2d44</item>
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
<item name="android:navigationBarColor">@color/settings_color_primary_dark</item>
</style>
</resources>