Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 74ce6e1d authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

Merge branch '3535-fix-oauth-relogin' into 'main'

fix: preserve sync settings on re-auth with improved code readability

See merge request !180
parents 621d098f b6f8ccb2
Loading
Loading
Loading
Loading
Loading
+11 −1
Original line number Diff line number Diff line
@@ -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("@")
    }
}
+42 −88
Original line number Diff line number Diff line
@@ -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<Boolean> {
            val result = MutableLiveData<Boolean>()
            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

                if (basicAuthAccount != null) {
                val shouldCopyExistingSettings = isReloginUsingOAuth || isExistingAccount

                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,58 +656,15 @@ 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
            }

            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 getOrCreateAccount(accountName: String, accountType: String) =
            findExistingMurenaAccount(accountName) ?: 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)
                }
        }

        /**
         * 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
                .firstOrNull { account ->
                    AccountUtils.matchesAccountName(account, accountName) &&
                            AccountUtils.isMurenaAccount(context, account)
                }

            val accountName = murenaAccount.name.substringBefore("@")
            return accountName == requestedAccountName
        }

        private fun updateAuthState(