diff --git a/app/build.gradle b/app/build.gradle
index 41d291c8829d0bb6e023b0040e0fd1ecd04d660f..e082e17c190b28733bbd2a46a5966feb4bfa7753 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,6 +9,7 @@ plugins {
id 'dagger.hilt.android.plugin'
id 'kotlin-android'
id 'kotlin-kapt' // remove as soon as Hilt supports KSP [https://issuetracker.google.com/179057202]
+ id 'kotlin-parcelize'
}
// Android configuration
@@ -28,7 +29,7 @@ android {
minSdkVersion 24 // Android 7.0
targetSdkVersion 33 // Android 13
- buildConfigField "String", "userAgent", "\"DAVx5\""
+ buildConfigField "String", "userAgent", "\"AccountManager\""
testInstrumentationRunner "at.bitfire.davdroid.CustomTestRunner"
}
@@ -146,6 +147,7 @@ configurations {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation "com.google.dagger:hilt-android:${versions.hilt}"
@@ -217,7 +219,7 @@ dependencies {
implementation "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2
implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2
implementation 'com.google.code.gson:gson:2.10.1'
- implementation("com.github.nextcloud:android-library:2.14.0") {
+ implementation("foundation.e:Nextcloud-Android-Library:1.0.5-release") {
exclude group: 'com.gitlab.bitfireAT', module: 'dav4jvm'
exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version
exclude group: 'com.squareup.okhttp3'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e519e3144d45acf1145c263a19a3545a7a0a5f89..2039cf52d24c53aa27e9e88d1ee273223b1e927c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -684,13 +684,22 @@
+
+
+
+
+
+
https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success
if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) {
return method;
@@ -380,6 +383,10 @@ public class InputStreamBinder extends IInputStreamService.Stub {
}
}
+ private static String getUserAgent() {
+ return "AccountManager-SSO(" + BuildConfig.VERSION_NAME + ")";
+ }
+
private Response processRequestV2(final NextcloudRequest request, final InputStream requestBodyInputStream)
throws UnsupportedOperationException,
com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException,
@@ -400,9 +407,10 @@ public class InputStreamBinder extends IInputStreamService.Stub {
new IllegalStateException("URL need to start with a /"));
}
- Uri serverUri = Uri.parse(AccountUtils.getBaseUrlForAccount(context, account));
- OwnCloudClient client = OwnCloudClientFactory.createOwnCloudClient(serverUri, context, true);
- client.setCredentials(OwnCloudCredentialsFactory.newBasicCredentials(account.name, getAcountPwd(account, context)));
+ OwnCloudClientManagerFactory.setUserAgent(getUserAgent());
+ final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton();
+ final OwnCloudAccount ownCloudAccount = new OwnCloudAccount(account, context);
+ final OwnCloudClient client = ownCloudClientManager.getClientFor(ownCloudAccount, context);
HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream);
@@ -428,6 +436,8 @@ public class InputStreamBinder extends IInputStreamService.Stub {
client.setFollowRedirects(true);
int status = client.executeMethod(method);
+ ownCloudClientManager.saveAllClients(context, account.type);
+
// Check if status code is 2xx --> https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success
if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) {
return new Response(method);
@@ -447,11 +457,6 @@ public class InputStreamBinder extends IInputStreamService.Stub {
}
}
- private static String getAcountPwd(Account account, Context ctx) throws AccountUtils.AccountNotFoundException {
- return AccountManager.get(ctx).getPassword(account);
- }
-
-
private boolean isValid(NextcloudRequest request) {
String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid());
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java
index 5b0607f5310efac8dc4d852ef1ce294ac3540866..d74e50312a5149bf3c3b1b455a7eb40e604d99b9 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java
+++ b/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java
@@ -35,7 +35,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
@@ -49,6 +48,7 @@ import java.util.logging.Level;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.log.Logger;
+import at.bitfire.davdroid.util.SsoUtils;
public class SsoGrantPermissionActivity extends AppCompatActivity {
@@ -115,7 +115,7 @@ public class SsoGrantPermissionActivity extends AppCompatActivity {
// create token
String token = UUID.randomUUID().toString().replaceAll("-", "");
- String userId = sanitizeUserId(account.name);
+ String userId = SsoUtils.INSTANCE.sanitizeUserId(account.name);
saveToken(token, account.name);
setResultData(token, userId, serverUrl);
@@ -136,23 +136,6 @@ public class SsoGrantPermissionActivity extends AppCompatActivity {
setResult(RESULT_OK, data);
}
- /**
- * Murena account's userId is set same as it's email address.
- * For old accounts (@e.email) userId = email.
- * For new accounts (@murena.io) userId is first part of email (ex: for email abc@murena.io, userId is abc).
- * For api requests, we needed to pass the actual userId. This method remove the unwanted part (@murena.io) from the userId
- */
- @NonNull
- private static String sanitizeUserId(@NonNull String userId) {
- final String murenaMailEndPart = "@murena.io";
-
- if (!userId.endsWith(murenaMailEndPart)) {
- return userId;
- }
-
- return userId.split(murenaMailEndPart)[0];
- }
-
@Nullable
private String getServerUrl() {
try {
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt
new file mode 100644
index 0000000000000000000000000000000000000000..87960a2698df4eab15bd9c9e6dedff493b6e1b6e
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright MURENA SAS 2024
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.network
+
+interface CookieParser {
+
+ fun cookiesAsString(): String
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2ae2934209660e1a90778295f573734b73f363ac
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright MURENA SAS 2024
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.network
+
+import android.accounts.Account
+import android.content.Context
+import at.bitfire.davdroid.R
+import okhttp3.CookieJar
+
+fun createCookieStore(context: Context, account: Account? = null): CookieJar {
+ if (account == null) {
+ return MemoryCookieStore()
+ }
+
+ val murenaAccountType = context.getString(R.string.eelo_account_type)
+ val murenaAddressBookAccountType = context.getString(R.string.account_type_eelo_address_book)
+
+ if (account.type in listOf(murenaAccountType, murenaAddressBookAccountType)) {
+ return PersistentCookieStore(context, account)
+ }
+
+ return MemoryCookieStore()
+}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt
index 5c38b98418d17c75cef6b584ac9a333f7064d00d..ac47042b323d3fd57bec0fc6fad9f7abdda00bee 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt
@@ -4,6 +4,7 @@
package at.bitfire.davdroid.network
+import android.accounts.Account
import android.content.Context
import android.os.Build
import android.security.KeyChain
@@ -17,6 +18,7 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
+import at.bitfire.davdroid.syncadapter.AccountUtils
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@@ -172,6 +174,7 @@ class HttpClient private constructor(
addAuthentication(
null,
credential,
+ account = accountSettings.account,
authStateCallback = { authState: AuthState ->
updateCredentials(accountSettings, authState, credential.clientSecret)
})
@@ -190,7 +193,7 @@ class HttpClient private constructor(
)
}
- fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder {
+ fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, account: Account? = null, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder {
if (credentials.userName != null && credentials.password != null) {
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.userName, credentials.password, insecurePreemptive)
orig.addNetworkInterceptor(authHandler)
@@ -208,6 +211,10 @@ class HttpClient private constructor(
orig.addNetworkInterceptor(bearerAuthInterceptor)
}
}
+
+ val accountForCookie = account ?: AccountUtils.getAccount(context, credentials.userName, host)
+ cookieStore = createCookieStore(context, accountForCookie)
+
return this
}
@@ -340,4 +347,12 @@ class HttpClient private constructor(
}
+ fun getCookieAsString(): String {
+ val cookieJar = okHttpClient.cookieJar
+ if (cookieJar is CookieParser) {
+ return cookieJar.cookiesAsString()
+ }
+
+ return ""
+ }
}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt
index 05e7ea63f0e859024e1c8403426324b70d2748df..3eacb5ce1016c68e56bc4b449596a750f91b284e 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt
@@ -4,19 +4,20 @@
package at.bitfire.davdroid.network
+import at.bitfire.davdroid.settings.AccountSettings
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import org.apache.commons.collections4.keyvalue.MultiKey
import org.apache.commons.collections4.map.HashedMap
import org.apache.commons.collections4.map.MultiKeyMap
-import java.util.*
+import java.util.LinkedList
/**
* Primitive cookie store that stores cookies in a (volatile) hash map.
* Will be sufficient for session cookies.
*/
-class MemoryCookieStore: CookieJar {
+class MemoryCookieStore : CookieJar, CookieParser {
/**
* Stored cookies. The multi-key consists of three parts: name, domain, and path.
@@ -56,4 +57,12 @@ class MemoryCookieStore: CookieJar {
return cookies
}
+ override fun cookiesAsString(): String {
+ if (storage.isEmpty) {
+ return ""
+ }
+
+ return storage.values.joinToString(separator = AccountSettings.COOKIE_SEPARATOR)
+ }
+
}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..37eada6e859cfd0549612da9163e979e5654147b
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright MURENA SAS 2024
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.network
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.content.Context
+import at.bitfire.davdroid.settings.AccountSettings
+import okhttp3.Cookie
+import okhttp3.CookieJar
+import okhttp3.HttpUrl
+
+class PersistentCookieStore(context: Context, private val account: Account): CookieJar {
+
+ private val accountManager = AccountManager.get(context.applicationContext)
+
+ override fun loadForRequest(url: HttpUrl): List {
+ return getCookieMap(url).values.filter {
+ it.matches(url)
+ }
+ }
+
+ override fun saveFromResponse(url: HttpUrl, cookies: List) {
+ val cookieList = cookies.filter {
+ it.expiresAt > System.currentTimeMillis()
+ }
+
+ if (cookieList.isEmpty()) {
+ return
+ }
+
+ val cookieMap = getCookieMap(url)
+
+ // replace old cookie with new one
+ cookieList.forEach {
+ cookieMap[it.name] = it
+ }
+
+ val cookieString = cookieMap.values.joinToString(separator = AccountSettings.COOKIE_SEPARATOR)
+ accountManager.setUserData(account, AccountSettings.COOKIE_KEY, cookieString)
+ }
+
+ private fun getCookieMap(url: HttpUrl): HashMap {
+ val result = HashMap()
+ val cookiesString = accountManager.getUserData(account, AccountSettings.COOKIE_KEY)?: return HashMap()
+
+ val cookies = cookiesString.split(AccountSettings.COOKIE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() }
+ .toTypedArray()
+
+ cookies.forEach {
+ val cookie = Cookie.parse(url, it) ?: return@forEach
+
+ if (cookie.expiresAt > System.currentTimeMillis()) {
+ result[cookie.name] = cookie
+ }
+ }
+
+ return result
+ }
+}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..12be4afe5b058fa39bd19b96607ac40e240470c7
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright MURENA SAS 2024
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.receiver
+
+import android.accounts.AccountManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import at.bitfire.davdroid.syncadapter.AccountUtils
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
+
+class AccountRemovedReceiver : BroadcastReceiver() {
+
+ companion object {
+ private const val ACCOUNT_REMOVAL_ACTION = "android.accounts.action.ACCOUNT_REMOVED"
+ }
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (context == null || intent == null || intent.action != ACCOUNT_REMOVAL_ACTION) {
+ return
+ }
+
+ val accountName = getAccountName(context, intent) ?: return
+
+ val ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton()
+ ownCloudClientManager.removeClientForByName(accountName)
+ }
+
+ private fun getAccountName(context: Context, intent: Intent): String? {
+ val accountType = intent.extras?.getString(AccountManager.KEY_ACCOUNT_TYPE)
+ if (accountType !in AccountUtils.getMainAccountTypes(context)) {
+ return null
+ }
+
+ return intent.extras?.getString(AccountManager.KEY_ACCOUNT_NAME)
+ }
+}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt
similarity index 97%
rename from app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt
rename to app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt
index dbbcd89ad82f71880dfbc42ba7bc4db8f709f759..d769750815e6293becf5334a2a931129bcf60fb6 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt
@@ -2,7 +2,7 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
-package at.bitfire.davdroid
+package at.bitfire.davdroid.receiver
import android.content.BroadcastReceiver
import android.content.Context
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt
index 72f8cba8b434b179cf640e6c2f37c87a38696a6d..befcb345883a0a0d6b4d8da76bafdc726899badf 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt
@@ -111,7 +111,8 @@ class DavResourceFinder(
return Configuration(
cardDavConfig, calDavConfig,
encountered401,
- logBuffer.toString()
+ logBuffer.toString(),
+ cookies = httpClient.getCookieAsString()
)
}
@@ -500,7 +501,8 @@ class DavResourceFinder(
val calDAV: ServiceInfo?,
val encountered401: Boolean,
- val logs: String
+ val logs: String,
+ val cookies: String? = null
) {
data class ServiceInfo(
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt
index 4bd79bbd2e78b571f29dd54f9587c25256b2974a..5cda84f5d989f1381d0ae45792135a2db2f3c6ab 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt
@@ -19,6 +19,7 @@ import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
import at.bitfire.davdroid.syncadapter.SyncUtils
+import at.bitfire.davdroid.util.SsoUtils
import at.bitfire.davdroid.util.setAndVerifyUserData
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
@@ -29,6 +30,8 @@ import dagger.hilt.components.SingletonComponent
import net.openid.appauth.AuthState
import org.apache.commons.lang3.StringUtils
import java.util.logging.Level
+import com.owncloud.android.lib.common.accounts.AccountUtils as NCAccountUtils
+
/**
* Manages settings of an account.
@@ -115,23 +118,29 @@ class AccountSettings(
const val CONTACTS_APP_INTERACTION = "z-app-generated--contactsinteraction--recent/"
+ const val COOKIE_KEY = "cookie_key"
+ const val COOKIE_SEPARATOR = ""
+
/** Static property to indicate whether AccountSettings migration is currently running.
* **Access must be `synchronized` with `AccountSettings::class.java`.** */
@Volatile
var currentlyUpdating = false
- fun initialUserData(credentials: Credentials?, baseURL: String? = null): Bundle {
+ fun initialUserData(credentials: Credentials?, baseURL: String? = null, cookies: String? = null): Bundle {
val bundle = Bundle()
bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
if (credentials != null) {
if (credentials.userName != null) {
bundle.putString(KEY_USERNAME, credentials.userName)
- bundle.putString("oc_display_name", credentials.userName)
+ bundle.putString(NCAccountUtils.Constants.KEY_DISPLAY_NAME, credentials.userName)
if (credentials.userName.contains("@")) {
bundle.putString(KEY_EMAIL_ADDRESS, credentials.userName)
}
+
+ val userId = SsoUtils.sanitizeUserId(credentials.userName)
+ bundle.putString(NCAccountUtils.Constants.KEY_USER_ID, userId)
}
if (credentials.certificateAlias != null) {
@@ -148,18 +157,14 @@ class AccountSettings(
}
if (!baseURL.isNullOrEmpty()) {
- bundle.putString("oc_base_url", getOCBaseUrl(baseURL))
+ bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, AccountUtils.getOwnCloudBaseUrl(baseURL))
}
- return bundle
- }
-
- private fun getOCBaseUrl(baseURL: String): String {
- if (baseURL.contains("remote.php")) {
- return baseURL.split("/remote.php")[0]
+ if (!cookies.isNullOrEmpty()) {
+ bundle.putString(COOKIE_KEY, cookies)
}
- return baseURL
+ return bundle
}
}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt
index 5dcaed9c6b7900f4ab1e60eed2a0f6dfa4d90cb0..d94c06b0c688ac83e32ab619a36ce1dcd79d8a73 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt
@@ -10,7 +10,9 @@ import android.content.Context
import android.os.Bundle
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
+import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.setAndVerifyUserData
+import com.owncloud.android.lib.common.accounts.AccountUtils
object AccountUtils {
@@ -128,4 +130,38 @@ object AccountUtils {
return accounts
}
+
+ fun getOwnCloudBaseUrl(baseURL: String): String {
+ if (baseURL.contains("/remote.php")) {
+ return baseURL.split("/remote.php")[0]
+ }
+
+ return baseURL
+ }
+
+ fun getAccount(context: Context, userName: String?, requestedBaseUrl: String?): Account? {
+ if (userName == null || requestedBaseUrl == null) {
+ return null
+ }
+
+ val baseUrl = getOwnCloudBaseUrl(requestedBaseUrl)
+
+ val accountManager = AccountManager.get(context.applicationContext)
+
+ val accounts = getMainAccounts(context)
+
+ for(account in accounts) {
+ val name = accountManager.getUserData(account, AccountSettings.KEY_USERNAME)
+ if (name != userName) {
+ continue
+ }
+
+ val url = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL)
+ if (url != null && getOwnCloudBaseUrl(url) == baseUrl) {
+ return account
+ }
+ }
+
+ return null
+ }
}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt
index 59e827633aa4dfb4a57415879f7e4f67f9c398b3..cbf740f65056e8ba1dab3914a994f5c52bbc458c 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt
@@ -4,12 +4,13 @@
package at.bitfire.davdroid.syncadapter
-import android.accounts.Account
-import android.accounts.AccountManager
import android.content.Context
import androidx.hilt.work.HiltWorker
-import androidx.work.*
-import at.bitfire.davdroid.R
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.Worker
+import androidx.work.WorkerParameters
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
@@ -49,15 +50,14 @@ class AccountsCleanupWorker @AssistedInject constructor(
override fun doWork(): Result {
lockAccountsCleanup()
try {
- val accountManager = AccountManager.get(applicationContext)
- cleanupAccounts(applicationContext, accountManager.accounts)
+ cleanupAccounts(applicationContext)
} finally {
unlockAccountsCleanup()
}
return Result.success()
}
- private fun cleanupAccounts(context: Context, accounts: Array) {
+ private fun cleanupAccounts(context: Context) {
Logger.log.log(Level.INFO, "Cleaning up accounts. Current accounts")
val mainAccountNames = HashSet()
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt
index 29543cd067b1a5cfe29f94c78a344c8a555adc09..ced0c5c1e75fe4c20a5b53ad4f14842473a35bd4 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt
@@ -49,6 +49,8 @@ import at.bitfire.davdroid.syncadapter.SyncAllAccountWorker
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.vcard4android.GroupMethod
import com.google.android.material.snackbar.Snackbar
+import com.nextcloud.android.utils.AccountManagerUtils
+import com.owncloud.android.lib.common.accounts.AccountTypeUtils
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -297,7 +299,7 @@ class AccountDetailsFragment : Fragment() {
val account = Account(name, accountType)
// create Android account
- val userData = AccountSettings.initialUserData(credentials, baseURL)
+ val userData = AccountSettings.initialUserData(credentials, baseURL, config.cookies)
Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData))
val accountManager = AccountManager.get(context)
@@ -315,10 +317,22 @@ class AccountDetailsFragment : Fragment() {
accountManager.setAuthToken(account, Constants.AUTH_TOKEN_TYPE, credentials?.authState?.accessToken)
}
+ var pass: String? = null
+
if (!credentials?.password.isNullOrEmpty()) {
+ pass = credentials?.password
+ }
+
+ pass?.let {
+ if (accountType == AccountManagerUtils.getAccountType(context)) {
+ accountManager.setAuthToken(account, AccountTypeUtils.getAuthTokenTypePass(account.type), it)
+ return@let
+ }
+
accountManager.setPassword(account, credentials?.password)
}
+
ContentResolver.setSyncAutomatically(account, context.getString(R.string.notes_authority), true)
ContentResolver.setSyncAutomatically(account, context.getString(R.string.email_authority), true)
ContentResolver.setSyncAutomatically(account, context.getString(R.string.media_authority), true)
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8edee609ef8c0ee019930caeb7b1a776e119bfc2
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright MURENA SAS 2024
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.util
+
+object SsoUtils {
+
+ private const val EELO_EMAIL_DOMAIN = "@e.email"
+
+ /**
+ * Murena account's userId is set same as it's email address.
+ * For old accounts (@e.email) userId = email.
+ * For new accounts (@murena.io) & other NC accounts userId is first part of email (ex: for email abc@murena.io, userId is abc).
+ * For api requests, we needed to pass the actual userId. This method remove the unwanted part (ex: @murena.io) from the userId
+ */
+ fun sanitizeUserId(param: String): String {
+ val userId = param.trim()
+ if (userId.endsWith(EELO_EMAIL_DOMAIN, ignoreCase = true)) {
+ return userId
+ }
+
+ if (userId.lastIndexOf("@") < 0) { // not email address
+ return userId
+ }
+
+ return userId.substringBefore("@")
+ }
+}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt
index 006b739d18fddd0785e8b474a60bb85f823914d2..79ef169cfe3bfee8707a99c8b89ad060423bedf1 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt
@@ -21,7 +21,7 @@ import at.bitfire.davdroid.util.DavUtils
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.RequestBody
-import okhttp3.internal.headersContentLength
+import okhttp3.internal.toLongOrDefault
import okio.BufferedSink
import org.apache.commons.io.FileUtils
import java.io.IOException
@@ -107,7 +107,7 @@ class StreamingFileDescriptor(
dav.get(mimeType?.toString() ?: DavUtils.MIME_TYPE_ACCEPT_ALL, null) { response ->
response.body?.use { body ->
if (response.isSuccessful) {
- val length = response.headersContentLength()
+ val length = response.headers["Content-Length"]?.toLongOrDefault(-1L) ?: -1L
notification.setContentTitle(context.getString(R.string.webdav_notification_download))
if (length == -1L)
diff --git a/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5a4f70cf513b27c54fb1c84f88cafca7c4c9fdea
--- /dev/null
+++ b/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright MURENA SAS 2024
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package at.bitfire.davdroid.util
+
+import org.junit.jupiter.api.Assertions.*
+
+import org.junit.jupiter.api.Test
+
+class SsoUtilsTest {
+
+ @Test
+ fun `test sanitizeUserId with empty input`() {
+ val userId = ""
+ val expected = ""
+ val actual = SsoUtils.sanitizeUserId(userId)
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `test sanitizeUserId with input without '@'`() {
+ val userId = "username"
+ val expected = "username"
+ val actual = SsoUtils.sanitizeUserId(userId)
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `test sanitizeUserId with case sensitivity`() {
+ val userId = "User@E.EMAIL"
+ val expected = "User@E.EMAIL"
+ val actual = SsoUtils.sanitizeUserId(userId)
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `test sanitizeUserId with leading or trailing spaces`() {
+ val userId = " user@domain.com "
+ val expected = "user"
+ val actual = SsoUtils.sanitizeUserId(userId)
+ assertEquals(expected, actual)
+ }
+}
diff --git a/build.gradle b/build.gradle
index de2cdb3e97d09377710b39041ba2c066b3f61803..e7341354fa58cc95cfa19c0bd857a21fdc305db3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ buildscript {
hilt: '2.48.1',
kotlin: '1.9.10', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion
// * com.google.devtools.ksp at the end of this file
- okhttp: '4.12.0',
+ okhttp: '5.0.0-alpha.11',
room: '2.5.2',
workManager: '2.9.0-rc01',
// Apache Commons versions