Reorganize and group modules
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,59 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
namespace = "de.mm20.launcher2.owncloud"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.materialcomponents.core)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.securitycrypto)
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
|
||||
api(project(":libs:webdav"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:i18n"))
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,16 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:label="@string/preference_category_services_nextcloud"
|
||||
android:taskAffinity="de.mm20.launcher2.nextcloud"
|
||||
android:parentActivityName="de.mm20.launcher2.ui.settings.SettingsActivity"
|
||||
android:theme="@style/OwncloudLoginTheme" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="de.mm20.launcher2.ui.settings.SettingsActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.mm20.launcher2.owncloud
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.owncloud.databinding.ActivityOwncloudLoginBinding
|
||||
import de.mm20.launcher2.owncloud.databinding.ActivityOwncloudLoginUsernamePasswordBinding
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private val owncloudClient = OwncloudClient(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityOwncloudLoginBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(binding.root)
|
||||
binding.nextButton.setOnClickListener {
|
||||
binding.serverUrlInputLayout.error = null
|
||||
lifecycleScope.launch {
|
||||
var url = binding.serverUrlInput.text.toString()
|
||||
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
|
||||
url = "https://$url"
|
||||
}
|
||||
if (url.isBlank()) {
|
||||
binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_url_empty)
|
||||
return@launch
|
||||
}
|
||||
if (owncloudClient.checkOwncloudInstallation(url)) {
|
||||
openLoginPage(url)
|
||||
} else {
|
||||
binding.serverUrlInputLayout.error = getString(R.string.owncloud_server_invalid_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLoginPage(url: String) {
|
||||
val binding = ActivityOwncloudLoginUsernamePasswordBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(binding.root)
|
||||
binding.loginButton.setOnClickListener {
|
||||
val username = binding.username.text.toString()
|
||||
val password = binding.password.text.toString()
|
||||
if (username.isEmpty()) {
|
||||
binding.usernameInputLayout.error = getString(R.string.owncloud_username_empty)
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
binding.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 {
|
||||
binding.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,201 @@
|
||||
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 getLoginIntent(): Intent {
|
||||
return Intent(context, LoginActivity::class.java)
|
||||
}
|
||||
|
||||
fun login(activity: Activity, requestCode: Int) {
|
||||
activity.startActivityForResult(getLoginIntent(), 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_continue" />
|
||||
|
||||
</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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user