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 50fb93e15b25f02eebcfb27075b339cf48844116..4cf9958c1025529e596dc8e0e32f3140479dbb8c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -13,7 +13,6 @@ 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 import com.owncloud.android.lib.common.accounts.AccountUtils.Constants object AccountUtils { @@ -191,4 +190,15 @@ object AccountUtils { return isPasswordNull && hasAuthStateData } + + fun matchesAccountName(account: Account, accountName: String?): Boolean { + if (accountName.isNullOrBlank()) { + return false + } + + // In Murena account's `name` field, non-SSO accounts contain domain name + // i.e. `user.name@example.com`, whereas SSO accounts have `user.name`. + // Only the name part without the domain needs to be extracted and compared. + return account.name.substringBefore("@") == accountName.substringBefore("@") + } } 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 8937aad8fbf62e8030a92ca34250e89c9cbd9774..aed9d9a73b468e47c2e29019fe14a9f4d4c9064c 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 @@ -30,9 +30,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.viewModelScope +import at.bitfire.davdroid.AccountSyncHelper import at.bitfire.davdroid.Constants import at.bitfire.davdroid.InvalidAccountException -import at.bitfire.davdroid.AccountSyncHelper import at.bitfire.davdroid.R import at.bitfire.davdroid.databinding.LoginAccountDetailsBinding import at.bitfire.davdroid.db.AppDatabase @@ -173,7 +173,7 @@ class AccountDetailsFragment : Fragment() { val idx = v.contactGroupMethod.selectedItemPosition val groupMethodName = resources.getStringArray(R.array.settings_contact_group_method_values)[idx] - val basicAuthAccount = findBasicAuthMurenaAccount(name, providedAccountType) + val basicAuthMurenaAccount = findBasicAuthMurenaAccount(name, providedAccountType) model.createOrUpdateAccount( requireActivity(), @@ -181,7 +181,7 @@ class AccountDetailsFragment : Fragment() { loginModel.credentials!!, config, GroupMethod.valueOf(groupMethodName), - basicAuthAccount, + basicAuthMurenaAccount, ).observe(viewLifecycleOwner, Observer { success -> if (success) { Toast.makeText(context, R.string.message_account_added_successfully, Toast.LENGTH_LONG).show() @@ -227,21 +227,13 @@ class AccountDetailsFragment : Fragment() { .firstOrNull() ?: return null return when { - !matchesAccountName(account, accountName) -> null !AccountUtils.isMurenaAccount(requireContext(), account) -> null + !AccountUtils.matchesAccountName(account, accountName) -> null AccountUtils.isLoggedInWithMurenaSso(requireContext(), account) -> null else -> account } } - private fun matchesAccountName(account: Account, accountName: String): Boolean { - // In Murena account's `name` field, non-SSO accounts contain domain name i.e. `user.name@example.com`, - // whereas SSO accounts have `user.name`. - // Only the name part without the domain needs to be extracted and compared. - - return account.name.substringBefore("@") == accountName.substringBefore("@") - } - private fun handlePostMurenaSsoMigrationOperations(name: String) { val authState = requireActivity().intent.getStringExtra(LoginActivity.AUTH_STATE) val isMigratedToMurenaSso = !authState.isNullOrBlank() @@ -324,7 +316,7 @@ class AccountDetailsFragment : Fragment() { credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod, - basicAuthAccount: Account? = null + basicAuthMurenaAccount: Account? = null ): LiveData { val result = MutableLiveData() viewModelScope.launch(Dispatchers.Default + NonCancellable) { @@ -357,7 +349,8 @@ class AccountDetailsFragment : Fragment() { } } - val account = getOrCreateAccount(name, accountType, basicAuthAccount) + // During Murena SSO migration, `basicAuthMurenaAccount` will be non-null. Otherwise, always null. + val account = basicAuthMurenaAccount ?: getOrCreateAccount(name, accountType) val userData = AccountSettings.initialUserData(credentials, baseURL, config.cookies, config.calDAV?.emails?.firstOrNull()) @@ -365,17 +358,22 @@ class AccountDetailsFragment : Fragment() { val accountManager = AccountManager.get(context) + val isExistingAccount = accountManager.getAccountsByType(account.type) + .any { AccountUtils.matchesAccountName(it, account.name) } + val isAccountUsingOAuth = accountType in AccountUtils.getOpenIdMainAccountTypes(context) && credentials?.authState != null - val isExisitingAccount = accountManager.getAccountsByType(account.type) - .any { it.name == credentials?.userName } - val isReauthAccount = basicAuthAccount != null || (isAccountUsingOAuth && isExisitingAccount) + val isReloginUsingOAuth = + basicAuthMurenaAccount == null && isExistingAccount && isAccountUsingOAuth + + val shouldCopyExistingSettings = isReloginUsingOAuth || isExistingAccount - if (basicAuthAccount != null) { + if (basicAuthMurenaAccount != null) { val authState = credentials?.authState if (authState != null) { + // Murena SSO migration happens here. updateAuthState(userData, account) Logger.log.info("Updated auth state for $account") @@ -445,40 +443,39 @@ class AccountDetailsFragment : Fragment() { var calendarSyncEnabled = true var tasksSyncEnabled = true - if (isReauthAccount) { - val accountToUpdate = basicAuthAccount ?: account - val oldSettings = AccountSettings(context, accountToUpdate) + if (shouldCopyExistingSettings) { + val oldSettings = AccountSettings(context, account) val addressBookAuthority = context.getString(R.string.address_books_authority) val taskProvider = TaskUtils.currentProvider(context) mediaSyncEnabled = ContentResolver.getSyncAutomatically( - accountToUpdate, + account, context.getString(R.string.media_authority) ) appDataSyncEnabled = ContentResolver.getSyncAutomatically( - accountToUpdate, + account, context.getString(R.string.app_data_authority) ) meteredEdriveSyncEnabled = ContentResolver.getSyncAutomatically( - accountToUpdate, + account, context.getString(R.string.metered_edrive_authority) ) notesSyncEnabled = ContentResolver.getSyncAutomatically( - accountToUpdate, + account, context.getString(R.string.notes_authority) ) emailSyncEnabled = ContentResolver.getSyncAutomatically( - accountToUpdate, + account, context.getString(R.string.email_authority) ) // --- Contacts sync logic --- contactsSyncable = - ContentResolver.getIsSyncable(accountToUpdate, addressBookAuthority) + ContentResolver.getIsSyncable(account, addressBookAuthority) val oldContactsSyncInterval = oldSettings.getSyncInterval(addressBookAuthority) contactsSyncEnabled = - ContentResolver.getSyncAutomatically(accountToUpdate, addressBookAuthority) + ContentResolver.getSyncAutomatically(account, addressBookAuthority) contactsSyncInterval = when { contactsSyncEnabled -> oldContactsSyncInterval ?: defaultSyncInterval @@ -489,11 +486,11 @@ class AccountDetailsFragment : Fragment() { // --- Calendar sync logic --- calendarSyncable = - ContentResolver.getIsSyncable(accountToUpdate, CalendarContract.AUTHORITY) + ContentResolver.getIsSyncable(account, CalendarContract.AUTHORITY) val oldCalendarSyncInterval = oldSettings.getSyncInterval(CalendarContract.AUTHORITY) calendarSyncEnabled = ContentResolver.getSyncAutomatically( - accountToUpdate, + account, CalendarContract.AUTHORITY ) calendarSyncInterval = when { @@ -505,9 +502,9 @@ class AccountDetailsFragment : Fragment() { // --- Tasks sync logic --- if (taskProvider != null) { tasksSyncable = - ContentResolver.getIsSyncable(accountToUpdate, taskProvider.authority) + ContentResolver.getIsSyncable(account, taskProvider.authority) tasksSyncEnabled = - ContentResolver.getSyncAutomatically(accountToUpdate, taskProvider.authority) + ContentResolver.getSyncAutomatically(account, taskProvider.authority) val oldTasksSyncInterval = oldSettings.getSyncInterval(taskProvider.authority) @@ -553,10 +550,10 @@ class AccountDetailsFragment : Fragment() { // Configure CardDAV service val addressBookAuthority = context.getString(R.string.address_books_authority) if (config.cardDAV != null) { - if (isReauthAccount) { - val accountToUpdate = basicAuthAccount ?: account + if (shouldCopyExistingSettings) { // Update existing service - val service = db.serviceDao().getByAccountAndType(accountToUpdate.name, Service.TYPE_CARDDAV) + val service = db.serviceDao() + .getByAccountAndType(account.name, Service.TYPE_CARDDAV) service?.let { it.authState = credentials?.authState?.jsonSerializeString() it.principal = config.cardDAV.principal @@ -592,10 +589,10 @@ class AccountDetailsFragment : Fragment() { // Configure CalDAV service if (config.calDAV != null) { - if (isReauthAccount) { - val accountToUpdate = basicAuthAccount ?: account + if (shouldCopyExistingSettings) { // Update existing service - val service = db.serviceDao().getByAccountAndType(accountToUpdate.name, Service.TYPE_CALDAV) + val service = db.serviceDao() + .getByAccountAndType(account.name, Service.TYPE_CALDAV) service?.let { it.authState = credentials?.authState?.jsonSerializeString() it.principal = config.calDAV.principal @@ -627,7 +624,7 @@ class AccountDetailsFragment : Fragment() { // if task provider present, set task sync interval and enable sync val taskProvider = TaskUtils.currentProvider(context) if (taskProvider != null) { - if (basicAuthAccount == null) { + if (basicAuthMurenaAccount == null) { ContentResolver.setIsSyncable(account, taskProvider.authority, tasksSyncable) } @@ -659,60 +656,17 @@ class AccountDetailsFragment : Fragment() { return result } - /** - * Gets or creates an account based on the provided parameters and existing accounts. - * - * @param accountName The name for the account - * @param accountType The type of account - * @param accountToUpdate An existing account to update, if any - * @return The account to use - */ - private fun getOrCreateAccount( - accountName: String, - accountType: String, - accountToUpdate: Account? - ): Account { - if (accountToUpdate != null) { - return accountToUpdate - } + private fun getOrCreateAccount(accountName: String, accountType: String) = + findExistingMurenaAccount(accountName) ?: Account(accountName, accountType) - val existingAccount = findExistingMurenaAccount() - - // If a Murena account exists and matches the requested account name, use it - if (existingAccount != null && isMurenaAccountMatchingRequestedName( - existingAccount, - accountName - ) - ) { - return existingAccount - } - - return Account(accountName, accountType) - } - - private fun findExistingMurenaAccount(): Account? { + private fun findExistingMurenaAccount(accountName: String): Account? { return AccountUtils.getOpenIdMainAccounts(context) - .firstOrNull { - it.type == context.getString(R.string.eelo_account_type) - && AccountUtils.isMurenaAccount(context, it) + .firstOrNull { account -> + AccountUtils.matchesAccountName(account, accountName) && + AccountUtils.isMurenaAccount(context, account) } } - /** - * Determines if an existing Murena account should be used based on the account name. - */ - private fun isMurenaAccountMatchingRequestedName( - murenaAccount: Account, - requestedAccountName: String - ): Boolean { - if (!murenaAccount.name.contains("@")) { - return false - } - - val accountName = murenaAccount.name.substringBefore("@") - return accountName == requestedAccountName - } - private fun updateAuthState( userData: Bundle, providedAccount: Account