diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt index 8ba43eb91df..ac0e2152171 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt @@ -58,7 +58,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( accountId = user.id, @@ -126,7 +126,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -252,7 +252,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -331,7 +331,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -421,7 +421,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -484,7 +484,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -532,7 +532,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, @@ -575,7 +575,7 @@ class ChatBlocksDaoTest { runTest { val user = createUserEntity("account1", "Account 1") usersDao.saveUser(user) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! conversationsDao.upsertConversations( account1.id, diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt index e81c6da9235..2815679aa5e 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt @@ -60,8 +60,8 @@ class ChatMessagesDaoTest { usersDao.saveUser(createUserEntity("account1", "Account 1")) usersDao.saveUser(createUserEntity("account2", "Account 2")) - val account1 = usersDao.getUserWithUserId("account1").blockingGet() - val account2 = usersDao.getUserWithUserId("account2").blockingGet() + val account1 = usersDao.getUserWithUserId("account1")!! + val account2 = usersDao.getUserWithUserId("account2")!! // Problem: lets say we want to update the conv list -> We don#t know the primary keys! // with account@token that would be easier! diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/user/UsersDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/user/UsersDaoTest.kt index 89be281f9c4..8ba7a9e54ce 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/data/user/UsersDaoTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/data/user/UsersDaoTest.kt @@ -13,6 +13,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.nextcloud.talk.data.source.local.TalkDatabase import com.nextcloud.talk.data.user.model.UserEntity import com.nextcloud.talk.models.json.push.PushConfigurationState +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -43,154 +45,168 @@ class UsersDaoTest { } @Test - fun saveAndGetUser() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - val id = usersDao.saveUser(user) + fun saveAndGetUser() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + val id = usersDao.saveUser(user) - val retrieved = usersDao.getUserWithId(id).blockingGet() - assertNotNull(retrieved) - assertEquals("user1", retrieved.userId) - } + val retrieved = usersDao.getUserWithId(id) + assertNotNull(retrieved) + assertEquals("user1", retrieved?.userId) + } @Test - fun saveUsersAndGetAll() { - val user1 = createUserEntity("user1", "Account 1", "https://server1.com") - val user2 = createUserEntity("user2", "Account 2", "https://server1.com") - usersDao.saveUsers(user1, user2) + fun saveUsersAndGetAll() = + runTest { + val user1 = createUserEntity("user1", "Account 1", "https://server1.com") + val user2 = createUserEntity("user2", "Account 2", "https://server1.com") + usersDao.saveUsers(user1, user2) - val users = usersDao.getUsers().blockingGet() - assertEquals(2, users.size) - } + val users = usersDao.getUsers() + assertEquals(2, users.size) + } @Test - fun getActiveUser() { - val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } - val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { current = false } - usersDao.saveUsers(user1, user2) + fun getActiveUser() = + runTest { + val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } + val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { current = false } + usersDao.saveUsers(user1, user2) - val active = usersDao.getActiveUser().blockingGet() - assertNotNull(active) - assertEquals("user1", active.userId) + val active = usersDao.getActiveUser() + assertNotNull(active) + assertEquals("user1", active?.userId) - assertEquals("user1", usersDao.getActiveUserSynchronously()?.userId) + assertEquals("user1", usersDao.getActiveUserSynchronously()?.userId) - val activeObs = usersDao.getActiveUserObservable().blockingFirst() - assertEquals("user1", activeObs.userId) - } + val activeFlow = usersDao.getActiveUserFlow().first() + assertEquals("user1", activeFlow?.userId) + } @Test - fun setUserAsActiveWithId() { - val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } - val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { current = false } - val id1 = usersDao.saveUser(user1) - val id2 = usersDao.saveUser(user2) - - usersDao.setUserAsActiveWithId(id2) - - val retrieved1 = usersDao.getUserWithId(id1).blockingGet() - val retrieved2 = usersDao.getUserWithId(id2).blockingGet() - assertEquals(false, retrieved1.current) - assertEquals(true, retrieved2.current) - } + fun setUserAsActiveWithId() = + runTest { + val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } + val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { current = false } + val id1 = usersDao.saveUser(user1) + val id2 = usersDao.saveUser(user2) + + usersDao.setUserAsActiveWithId(id2) + + val retrieved1 = usersDao.getUserWithId(id1) + val retrieved2 = usersDao.getUserWithId(id2) + assertEquals(false, retrieved1?.current) + assertEquals(true, retrieved2?.current) + } @Test - fun deleteUser() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - val id = usersDao.saveUser(user) - val savedUser = usersDao.getUserWithId(id).blockingGet() + fun deleteUser() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + val id = usersDao.saveUser(user) + val savedUser = usersDao.getUserWithId(id) - usersDao.deleteUser(savedUser) + usersDao.deleteUser(savedUser!!) - val users = usersDao.getUsers().blockingGet() - assertTrue(users.isEmpty()) - } + val users = usersDao.getUsers() + assertTrue(users.isEmpty()) + } @Test - fun updateUser() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - val id = usersDao.saveUser(user) - val savedUser = usersDao.getUserWithId(id).blockingGet() + fun updateUser() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + val id = usersDao.saveUser(user) + val savedUser = usersDao.getUserWithId(id)!! - savedUser.displayName = "New Display Name" - usersDao.updateUser(savedUser) + savedUser.displayName = "New Display Name" + usersDao.updateUser(savedUser) - val retrieved = usersDao.getUserWithId(id).blockingGet() - assertEquals("New Display Name", retrieved.displayName) - } + val retrieved = usersDao.getUserWithId(id) + assertEquals("New Display Name", retrieved?.displayName) + } @Test - fun getScheduledForDeletion() { - val user1 = createUserEntity("user1", "Account 1", "https://server1.com").apply { scheduledForDeletion = true } - val user2 = createUserEntity("user2", "Account 2", "https://server1.com").apply { scheduledForDeletion = false } - usersDao.saveUsers(user1, user2) - - val scheduled = usersDao.getUsersScheduledForDeletion().blockingGet() - assertEquals(1, scheduled.size) - assertEquals("user1", scheduled[0].userId) - - val notScheduled = usersDao.getUsersNotScheduledForDeletion().blockingGet() - assertEquals(1, notScheduled.size) - assertEquals("user2", notScheduled[0].userId) - - val all = usersDao.getUsers().blockingGet() - assertEquals(1, all.size) - assertEquals("user2", all[0].userId) - } + fun getScheduledForDeletion() = + runTest { + val user1 = createUserEntity("user1", "Account 1", "https://server1.com") + .apply { scheduledForDeletion = true } + val user2 = createUserEntity("user2", "Account 2", "https://server1.com") + .apply { scheduledForDeletion = false } + usersDao.saveUsers(user1, user2) + + val scheduled = usersDao.getUsersScheduledForDeletion() + assertEquals(1, scheduled.size) + assertEquals("user1", scheduled[0].userId) + + val notScheduled = usersDao.getUsersNotScheduledForDeletion() + assertEquals(1, notScheduled.size) + assertEquals("user2", notScheduled[0].userId) + + val all = usersDao.getUsers() + assertEquals(1, all.size) + assertEquals("user2", all[0].userId) + } @Test - fun getUserWithUserId() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - usersDao.saveUser(user) + fun getUserWithUserId() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + usersDao.saveUser(user) - val retrieved = usersDao.getUserWithUserId("user1").blockingGet() - assertNotNull(retrieved) - assertEquals("Account 1", retrieved.username) + val retrieved = usersDao.getUserWithUserId("user1") + assertNotNull(retrieved) + assertEquals("Account 1", retrieved?.username) - val nonexistent = usersDao.getUserWithUserId("nonexistent").blockingGet() - assertNull(nonexistent) - } + val nonexistent = usersDao.getUserWithUserId("nonexistent") + assertNull(nonexistent) + } @Test - fun getUserWithUsernameAndServer() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - usersDao.saveUser(user) + fun getUserWithUsernameAndServer() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + usersDao.saveUser(user) - val retrieved = usersDao.getUserWithUsernameAndServer("Account 1", "https://server1.com").blockingGet() - assertNotNull(retrieved) - assertEquals("user1", retrieved.userId) - } + val retrieved = usersDao.getUserWithUsernameAndServer("Account 1", "https://server1.com") + assertNotNull(retrieved) + assertEquals("user1", retrieved?.userId) + } @Test - fun updatePushState() { - val user = createUserEntity("user1", "Account 1", "https://server1.com") - val id = usersDao.saveUser(user) + fun updatePushState() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com") + val id = usersDao.saveUser(user) - val newState = PushConfigurationState("token", "id", "sig", "key", true) - val updatedCount = usersDao.updatePushState(id, newState).blockingGet() - assertEquals(1, updatedCount) + val newState = PushConfigurationState("token", "id", "sig", "key", true) + val updatedCount = usersDao.updatePushState(id, newState) + assertEquals(1, updatedCount) - val retrieved = usersDao.getUserWithId(id).blockingGet() - assertEquals(newState, retrieved.pushConfigurationState) - } + val retrieved = usersDao.getUserWithId(id) + assertEquals(newState, retrieved?.pushConfigurationState) + } @Test - fun setActiveNonExistentUser() { - val count = usersDao.setUserAsActiveWithId(9999) - assertEquals(0, count) - } + fun setActiveNonExistentUser() = + runTest { + val count = usersDao.setUserAsActiveWithId(9999) + assertEquals(0, count) + } @Test - fun deleteActiveUser() { - val user = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } - val id = usersDao.saveUser(user) - val savedUser = usersDao.getUserWithId(id).blockingGet() - - usersDao.deleteUser(savedUser) - - val active = usersDao.getActiveUser().blockingGet() - assertNull(active) - assertNull(usersDao.getActiveUserSynchronously()) - } + fun deleteActiveUser() = + runTest { + val user = createUserEntity("user1", "Account 1", "https://server1.com").apply { current = true } + val id = usersDao.saveUser(user) + val savedUser = usersDao.getUserWithId(id)!! + + usersDao.deleteUser(savedUser) + + val active = usersDao.getActiveUser() + assertNull(active) + assertNull(usersDao.getActiveUserSynchronously()) + } private fun createUserEntity(userId: String, userName: String, server: String) = UserEntity( diff --git a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt index 90ae09c9670..143f36007fb 100644 --- a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt @@ -53,11 +53,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PASSWORD import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.rx2.await -import kotlinx.coroutines.rx2.awaitSingle -import kotlinx.coroutines.withContext import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.net.CookieManager @@ -140,15 +136,9 @@ class AccountVerificationActivity : BaseActivity() { return !TextUtils.isEmpty(originalProtocol) && !baseUrl.startsWith(originalProtocol) } - private suspend fun getUser(id: Long): User = - withContext(Dispatchers.IO) { - userManager.getUserWithId(id).awaitSingle() - } + private suspend fun getUser(id: Long): User = userManager.getUserWithIdSuspend(id)!! - private suspend fun getAllUsers(): List = - withContext(Dispatchers.IO) { - userManager.users.await() - } + private suspend fun getAllUsers(): List = userManager.getUsers() /** Appends [resId] as a new line to the progress text already shown to the user. */ @SuppressLint("SetTextI18n") @@ -229,24 +219,22 @@ class AccountVerificationActivity : BaseActivity() { @Suppress("TooGenericExceptionCaught") private suspend fun storeProfile(displayName: String?, userId: String, capabilitiesOverall: CapabilitiesOverall) { try { - val user = withContext(Dispatchers.IO) { - userManager.storeProfile( - username, - UserManager.UserAttributes( - id = null, - serverUrl = baseUrl, - currentUser = false, - userId = userId, - token = token, - displayName = displayName, - pushConfigurationState = null, - capabilities = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.capabilities), - serverVersion = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.serverVersion), - certificateAlias = appPreferences.temporaryClientCertAlias, - externalSignalingServer = null - ) - ).awaitSingle() - } + val user = userManager.storeProfileSuspend( + username, + UserManager.UserAttributes( + id = null, + serverUrl = baseUrl, + currentUser = false, + userId = userId, + token = token, + displayName = displayName, + pushConfigurationState = null, + capabilities = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.capabilities), + serverVersion = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.serverVersion), + certificateAlias = appPreferences.temporaryClientCertAlias, + externalSignalingServer = null + ) + )!! internalAccountId = user.id!! setupPushNotifications() } catch (e: Exception) { @@ -441,7 +429,7 @@ class AccountVerificationActivity : BaseActivity() { val userToSetAsActive = getUser(internalAccountId) Log.d(TAG, "userToSetAsActive: " + userToSetAsActive.username) - if (withContext(Dispatchers.IO) { userManager.setUserAsActive(userToSetAsActive).await() }) { + if (userManager.setUserAsActiveSuspend(userToSetAsActive)) { if (getAllUsers().size > 1 && isAccountImport) { ApplicationWideMessageHolder.getInstance().messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED @@ -471,7 +459,7 @@ class AccountVerificationActivity : BaseActivity() { @SuppressLint("CheckResult") private suspend fun deleteUserAndStartServerSelection(userId: Long) { - withContext(Dispatchers.IO) { userManager.scheduleUserForDeletionWithId(userId).await() } + userManager.scheduleUserForDeletionWithIdSuspend(userId) val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) .setExpeditedIfSupported() .build() diff --git a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt index 35d2c1e1ffd..8a30498bb90 100644 --- a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt @@ -51,6 +51,7 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.runBlocking import java.security.cert.CertificateException import javax.inject.Inject @@ -115,7 +116,7 @@ class ServerSelectionActivity : BaseActivity() { binding.certTextView.visibility = View.GONE } - val loggedInUsers = userManager.users.blockingGet() + val loggedInUsers = runBlocking { userManager.getUsers() } val availableAccounts = AccountUtils.findAvailableAccountsOnDevice(loggedInUsers) if (isImportAccountNameSet() && availableAccounts.isNotEmpty()) { diff --git a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt index 1912aa231af..9bf35e20448 100644 --- a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt @@ -12,6 +12,7 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.os.Bundle import androidx.core.graphics.drawable.toDrawable +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import autodagger.AutoInjector import com.nextcloud.talk.R @@ -31,6 +32,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME +import kotlinx.coroutines.launch import java.net.CookieManager import javax.inject.Inject @@ -93,50 +95,56 @@ class SwitchAccountActivity : BaseActivity() { if (isAccountImport) { reauthorizeFromImport(item.account) } else { - if (userManager.setUserAsActive(item.user!!).blockingGet()) { - DirectShareHelper.removeAllShareTargetShortcuts(this@SwitchAccountActivity) - cookieManager.cookieStore.removeAll() - finish() + lifecycleScope.launch { + if (userManager.setUserAsActiveSuspend(item.user!!)) { + DirectShareHelper.removeAllShareTargetShortcuts(this@SwitchAccountActivity) + cookieManager.cookieStore.removeAll() + finish() + } } } } - var participant: Participant - - if (!isAccountImport) { - for (user in userManager.users.blockingGet()) { - if (!user.current) { - val userId: String? = if (user.userId != null) { - user.userId - } else { - user.username + lifecycleScope.launch { + var participant: Participant + + if (!isAccountImport) { + for (user in userManager.getUsers()) { + if (!user.current) { + val userId: String? = if (user.userId != null) { + user.userId + } else { + user.username + } + participant = Participant() + participant.actorType = Participant.ActorType.USERS + participant.actorId = userId + participant.displayName = user.displayName + userItems.add(AdvancedUserItem(participant, user, null, 0)) } + } + } else { + var account: Account + var importAccount: ImportAccount + var user: User + for (accountObject in findAvailableAccountsOnDevice(userManager.getUsers())) { + account = accountObject + importAccount = getInformationFromAccount(account) participant = Participant() participant.actorType = Participant.ActorType.USERS - participant.actorId = userId - participant.displayName = user.displayName - userItems.add(AdvancedUserItem(participant, user, null, 0)) + participant.actorId = importAccount.getUsername() + participant.displayName = importAccount.getUsername() + user = User() + user.baseUrl = importAccount.getBaseUrl() + userItems.add(AdvancedUserItem(participant, user, account, 0)) } } - } else { - var account: Account - var importAccount: ImportAccount - var user: User - for (accountObject in findAvailableAccountsOnDevice(userManager.users.blockingGet())) { - account = accountObject - importAccount = getInformationFromAccount(account) - participant = Participant() - participant.actorType = Participant.ActorType.USERS - participant.actorId = importAccount.getUsername() - participant.displayName = importAccount.getUsername() - user = User() - user.baseUrl = importAccount.getBaseUrl() - userItems.add(AdvancedUserItem(participant, user, account, 0)) - } + adapter!!.submitList(userItems) + prepareViews() } - adapter!!.submitList(userItems) + } else { + prepareViews() } - prepareViews() } private fun prepareViews() { diff --git a/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt index 2a9e406e0c2..9699778cb15 100644 --- a/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt @@ -175,7 +175,7 @@ class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLogin /** * Returns bundle if user is not scheduled for deletion or doesn't already exist, null otherwise */ - fun parseAndLogin(loginData: LoginCompletion): Bundle? { + suspend fun parseAndLogin(loginData: LoginCompletion): Bundle? { if (local.checkIfUserIsScheduledForDeletion(loginData)) { // however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it. local.startAccountRemovalWorker() diff --git a/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt index 64f36e90c0a..0401a92bcb7 100644 --- a/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt @@ -17,6 +17,7 @@ import com.nextcloud.talk.account.data.model.LoginCompletion import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.preferences.AppPreferences +import kotlinx.coroutines.runBlocking // local datasource for communicating with room through account manager // crucial for making sure the login process interacts with the db as expected. @@ -27,7 +28,7 @@ class LocalLoginDataSource(val userManager: UserManager, val appPreferences: App if (currentUser != null) { currentUser.clientCertificate = appPreferences.temporaryClientCertAlias currentUser.token = loginData.appPassword - userManager.updateOrCreateUser(currentUser) + runBlocking { userManager.updateOrCreateUserSuspend(currentUser) } } } @@ -40,9 +41,9 @@ class LocalLoginDataSource(val userManager: UserManager, val appPreferences: App return WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) } - fun checkIfUserIsScheduledForDeletion(data: LoginCompletion): Boolean = - userManager.checkIfUserIsScheduledForDeletion(data.loginName, data.server).blockingGet() + suspend fun checkIfUserIsScheduledForDeletion(data: LoginCompletion): Boolean = + userManager.checkIfUserIsScheduledForDeletionSuspend(data.loginName, data.server) - fun checkIfUserExists(data: LoginCompletion): Boolean = - userManager.checkIfUserExists(data.loginName, data.server).blockingGet() + suspend fun checkIfUserExists(data: LoginCompletion): Boolean = + userManager.checkIfUserExistsSuspend(data.loginName, data.server) } diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 85efdc008d2..024378e80ef 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -19,6 +19,7 @@ import android.util.Log import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.BuildConfig @@ -41,11 +42,10 @@ import com.nextcloud.talk.utils.ShortcutManagerHelper import com.nextcloud.talk.utils.UnifiedPushUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN -import io.reactivex.SingleObserver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import javax.inject.Inject @@ -229,6 +229,7 @@ class MainActivity : handleIntent(intent) } + @Suppress("TooGenericExceptionCaught") private fun handleIntent(intent: Intent) { // Handle deep links first (nextcloudtalk:// scheme) if (handleDeepLink(intent)) { @@ -239,30 +240,28 @@ class MainActivity : val internalUserId = intent.extras?.getLong(BundleKeys.KEY_INTERNAL_USER_ID) - var user: User? = null - if (internalUserId != null && internalUserId != 0L) { - user = userManager.getUserWithId(internalUserId).blockingGet() - } - - if (user != null && userManager.setUserAsActive(user).blockingGet()) { - if (intent.hasExtra(BundleKeys.KEY_REMOTE_TALK_SHARE)) { - if (intent.getBooleanExtra(BundleKeys.KEY_REMOTE_TALK_SHARE, false)) { - val invitationsIntent = Intent(this, InvitationsActivity::class.java) - startActivity(invitationsIntent) - } + lifecycleScope.launch { + val user: User? = if (internalUserId != null && internalUserId != 0L) { + userManager.getUserWithIdSuspend(internalUserId) } else { - val chatIntent = Intent(context, ChatActivity::class.java) - chatIntent.putExtras(intent.extras!!) - startActivity(chatIntent) + null } - } else { - userManager.users.subscribe(object : SingleObserver> { - override fun onSubscribe(d: Disposable) { - // unused atm - } - override fun onSuccess(users: List) { - if (isFinishing || isDestroyed) return + if (user != null && userManager.setUserAsActiveSuspend(user)) { + if (intent.hasExtra(BundleKeys.KEY_REMOTE_TALK_SHARE)) { + if (intent.getBooleanExtra(BundleKeys.KEY_REMOTE_TALK_SHARE, false)) { + val invitationsIntent = Intent(this@MainActivity, InvitationsActivity::class.java) + startActivity(invitationsIntent) + } + } else { + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(intent.extras!!) + startActivity(chatIntent) + } + } else { + try { + val users = userManager.getUsers() + if (isFinishing || isDestroyed) return@launch if (users.isNotEmpty()) { if (appPreferences.useUnifiedPush) { @@ -270,19 +269,13 @@ class MainActivity : } else { ClosedInterfaceImpl().setUpPushTokenRegistration() } - runOnUiThread { - if (isFinishing || isDestroyed) return@runOnUiThread - openConversationList() - } + if (isFinishing || isDestroyed) return@launch + openConversationList() } else { - runOnUiThread { - if (isFinishing || isDestroyed) return@runOnUiThread - launchServerSelection() - } + if (isFinishing || isDestroyed) return@launch + launchServerSelection() } - } - - override fun onError(e: Throwable) { + } catch (e: Exception) { Log.e(TAG, "Error loading existing users", e) Toast.makeText( context, @@ -290,7 +283,7 @@ class MainActivity : Toast.LENGTH_SHORT ).show() } - }) + } } } @@ -303,74 +296,71 @@ class MainActivity : * @param intent The intent to process * @return true if the intent was handled as a deep link, false otherwise */ + @Suppress("TooGenericExceptionCaught") private fun handleDeepLink(intent: Intent): Boolean { val deepLinkResult = intent.data?.let { DeepLinkHandler.parseDeepLink(it) } ?: return false - val disposable = userManager.users - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { users -> - if (isFinishing || isDestroyed) return@subscribe + lifecycleScope.launch { + try { + val users = userManager.getUsers() + if (isFinishing || isDestroyed) return@launch - if (users.isEmpty()) { - launchServerSelection() - return@subscribe - } + if (users.isEmpty()) { + launchServerSelection() + return@launch + } + + val targetUser = resolveTargetUser(users, deepLinkResult) - val targetUser = resolveTargetUser(users, deepLinkResult) + if (targetUser == null) { + Toast.makeText( + context, + context.resources.getString(R.string.nc_no_account_for_server), + Toast.LENGTH_LONG + ).show() + openConversationList() + return@launch + } - if (targetUser == null) { - Toast.makeText( + if (userManager.setUserAsActiveSuspend(targetUser)) { + // Report shortcut usage for ranking + targetUser.id?.let { userId -> + ShortcutManagerHelper.reportShortcutUsed( context, - context.resources.getString(R.string.nc_no_account_for_server), - Toast.LENGTH_LONG - ).show() - openConversationList() - return@subscribe + deepLinkResult.roomToken, + userId + ) } - if (userManager.setUserAsActive(targetUser).blockingGet()) { - // Report shortcut usage for ranking - targetUser.id?.let { userId -> - ShortcutManagerHelper.reportShortcutUsed( - context, - deepLinkResult.roomToken, - userId - ) - } + if (isFinishing || isDestroyed) return@launch - if (isFinishing || isDestroyed) return@subscribe + // Open conversation list first so back press shows correct user's conversations + val listIntent = Intent(context, ConversationsListActivity::class.java) + listIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + listIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, targetUser.id) - // Open conversation list first so back press shows correct user's conversations - val listIntent = Intent(context, ConversationsListActivity::class.java) - listIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - listIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, targetUser.id) - - val chatIntent = Intent(context, ChatActivity::class.java) - chatIntent.putExtra(KEY_ROOM_TOKEN, deepLinkResult.roomToken) - chatIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, targetUser.id) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtra(KEY_ROOM_TOKEN, deepLinkResult.roomToken) + chatIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID, targetUser.id) - startActivities(arrayOf(listIntent, chatIntent)) - } else { - Toast.makeText( - context, - context.resources.getString(R.string.nc_common_error_sorry), - Toast.LENGTH_SHORT - ).show() - } - }, - { e -> - Log.e(TAG, "Error loading users for deep link", e) - if (isFinishing || isDestroyed) return@subscribe + startActivities(arrayOf(listIntent, chatIntent)) + } else { Toast.makeText( context, context.resources.getString(R.string.nc_common_error_sorry), Toast.LENGTH_SHORT ).show() } - ) - disposables.add(disposable) + } catch (e: Exception) { + Log.e(TAG, "Error loading users for deep link", e) + if (isFinishing || isDestroyed) return@launch + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_SHORT + ).show() + } + } return true } diff --git a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt index abcd3d9d022..25c37754330 100644 --- a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt @@ -38,6 +38,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ONE_TO_ONE import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import kotlinx.coroutines.runBlocking import okhttp3.Cache import java.io.IOException import javax.inject.Inject @@ -77,7 +78,7 @@ class CallNotificationActivity : CallBaseActivity() { hideNavigationIfNoPipAvailable() handleExtras() - userBeingCalled = userManager.getUserWithId(internalUserId).blockingGet() + userBeingCalled = runBlocking { userManager.getUserWithIdSuspend(internalUserId) } setupCallTypeDescription() binding!!.conversationNameTextView.text = displayName diff --git a/app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt b/app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt index 91e5f5e6185..2e4321d06c6 100644 --- a/app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt +++ b/app/src/main/java/com/nextcloud/talk/chooseaccount/ChooseAccountDialogCompose.kt @@ -46,6 +46,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -95,6 +96,7 @@ import com.nextcloud.talk.utils.CapabilitiesUtil import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld +import kotlinx.coroutines.launch import java.net.CookieManager import javax.inject.Inject @@ -142,16 +144,18 @@ class ChooseAccountDialogCompose { val showStatusMessageSheet = rememberSaveable { mutableStateOf(false) } val context = LocalContext.current val statusViewState by statusViewModel.statusViewState.collectAsStateWithLifecycle() - val invitationsState by invitationsViewModel.getInvitationsViewState.collectAsStateWithLifecycle() + val invitationsStateByUser by invitationsViewModel.invitationsStateByUser.collectAsStateWithLifecycle() val isOnline by networkMonitor.isOnline.collectAsStateWithLifecycle() val currentUser = currentUserProvider.currentUser.blockingGet()!! val isStatusAvailable = CapabilitiesUtil.isUserStatusAvailable(currentUser) ecosystemManager = EcosystemManager(activity) LaunchedEffect(currentUser) { - val users = userManager.users.blockingGet() + val users = userManager.getUsers() + userItems.clear() users.forEach { user -> if (!user.current) { + addAccountToList(user, pendingInvitations = 0) invitationsViewModel.getInvitations(user) } } @@ -159,9 +163,8 @@ class ChooseAccountDialogCompose { statusViewModel.getStatus() } } - LaunchedEffect(invitationsState) { - userItems.clear() - setupAccounts(invitationsState) + LaunchedEffect(invitationsStateByUser) { + updatePendingInvitationCounts(invitationsStateByUser) } handleStatusState(statusViewState, status) MaterialTheme(colorScheme = colorScheme) { @@ -235,11 +238,12 @@ class ChooseAccountDialogCompose { } } - private fun setupAccounts(invitationsUiState: InvitationsViewModel.ViewState) { - userManager.users.blockingGet().forEach { user -> - if (!user.current) { - val pendingCount = getPendingInvitations(invitationsUiState) - addAccountToList(user, pendingCount) + private fun updatePendingInvitationCounts(statesByUserId: Map) { + statesByUserId.forEach { (userId, state) -> + val pendingCount = getPendingInvitations(state) + val index = userItems.indexOfFirst { it.user.id == userId } + if (index >= 0 && userItems[index].pendingInvitation != pendingCount) { + userItems[index] = userItems[index].copy(pendingInvitation = pendingCount) } } } @@ -286,17 +290,20 @@ class ChooseAccountDialogCompose { @Composable private fun AccountRow(userItem: AccountItem, activity: Activity, onSelected: () -> Unit) { + val scope = rememberCoroutineScope() Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .clickable { - if (userManager.setUserAsActive(userItem.user).blockingGet()) { - cookieManager.cookieStore.removeAll() - val intent = Intent(activity, ConversationsListActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - activity.startActivity(intent) - onSelected() + scope.launch { + if (userManager.setUserAsActiveSuspend(userItem.user)) { + cookieManager.cookieStore.removeAll() + val intent = Intent(activity, ConversationsListActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + activity.startActivity(intent) + onSelected() + } } } .padding(8.dp) diff --git a/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt index d4b04a0020b..8f4ab72356f 100644 --- a/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/contextchat/ContextChatViewModel.kt @@ -14,7 +14,7 @@ import autodagger.AutoInjector import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource import com.nextcloud.talk.models.json.chat.ChatMessageJson -import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.database.user.CurrentUserProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -25,7 +25,7 @@ class ContextChatViewModel @Inject constructor(private val chatNetworkDataSource ViewModel() { @Inject - lateinit var userManager: UserManager + lateinit var currentUserProvider: CurrentUserProvider var threadId: String? = null @@ -43,7 +43,7 @@ class ContextChatViewModel @Inject constructor(private val chatNetworkDataSource title: String ) { viewModelScope.launch { - val user = userManager.currentUser.blockingGet() + val user = currentUserProvider.getCurrentUser().getOrThrow() if (!user.hasSpreedFeatureCapability("chat-get-context") || !user.hasSpreedFeatureCapability("federation-v1") diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 1b41c8082ca..ba61dbbbec7 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -118,7 +118,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.rx2.await +import kotlinx.coroutines.runBlocking import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import retrofit2.HttpException @@ -200,7 +200,7 @@ class ConversationsListActivity : BaseActivity() { val targetUserId = intent.getLongExtra(KEY_INTERNAL_USER_ID, 0L) currentUser = if (targetUserId != 0L) { - userManager.getUserWithId(targetUserId).blockingGet() + runBlocking { userManager.getUserWithIdSuspend(targetUserId) }!! } else { currentUserProviderOld.currentUser.blockingGet() } @@ -343,20 +343,22 @@ class ConversationsListActivity : BaseActivity() { object : AccountReceiverCallback { @SuppressLint("UseKtx") override fun onAccountReceived(accountName: String) { - val users = userManager.users.blockingGet() val baseUrl = accountName.substringAfterLast("@") - val accountName = accountName.substringBeforeLast("@") - val user = users.firstOrNull { user -> - user.username == accountName && baseUrl == user.baseUrl?.toUri()?.host - } - if (user != null) { - userManager.setUserAsActive(user) - val intent = Intent(context, ConversationsListActivity::class.java) - startActivity(intent) - } else { - showSnackbar(getString(R.string.nc_no_account_found)) + val trimmedAccountName = accountName.substringBeforeLast("@") + lifecycleScope.launch { + val users = userManager.getUsers() + val user = users.firstOrNull { user -> + user.username == trimmedAccountName && baseUrl == user.baseUrl?.toUri()?.host + } + if (user != null) { + userManager.setUserAsActiveSuspend(user) + val intent = Intent(context, ConversationsListActivity::class.java) + startActivity(intent) + } else { + showSnackbar(getString(R.string.nc_no_account_found)) + } } - Log.d(TAG, accountName) + Log.d(TAG, trimmedAccountName) } override fun onAccountError(reason: String) { @@ -425,7 +427,7 @@ class ConversationsListActivity : BaseActivity() { } lifecycleScope.launch { - hasMultipleAccountsState.value = userManager.users.await().size > 1 + hasMultipleAccountsState.value = userManager.getUsers().size > 1 } conversationsListViewModel.setHideRoomToken(intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM)) fetchRooms() @@ -705,7 +707,7 @@ class ConversationsListActivity : BaseActivity() { .setCancelable(false) .setNegativeButton(R.string.close, null) - if (resources!!.getBoolean(R.bool.multiaccount_support) && userManager.users.blockingGet().size > 1) { + if (resources!!.getBoolean(R.bool.multiaccount_support) && runBlocking { userManager.getUsers() }.size > 1) { dialogBuilder.setPositiveButton(R.string.nc_switch_account) { _, _ -> showChooseAccountDialog() } @@ -1365,44 +1367,45 @@ class ConversationsListActivity : BaseActivity() { ) } - @SuppressLint("CheckResult") private fun deleteUserAndRestartApp() { - userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() - val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) - .setExpeditedIfSupported() - .build() - WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + lifecycleScope.launch { + userManager.scheduleUserForDeletionWithIdSuspend(currentUser!!.id!!) + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) + .setExpeditedIfSupported() + .build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) - WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) - .observeForever { workInfo: WorkInfo? -> + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo? -> - when (workInfo?.state) { - WorkInfo.State.SUCCEEDED -> { - val text = String.format( - context.resources.getString(R.string.nc_deleted_user), - currentUser!!.displayName - ) - Toast.makeText( - context, - text, - Toast.LENGTH_LONG - ).show() - restartApp() - } + when (workInfo?.state) { + WorkInfo.State.SUCCEEDED -> { + val text = String.format( + context.resources.getString(R.string.nc_deleted_user), + currentUser!!.displayName + ) + Toast.makeText( + context, + text, + Toast.LENGTH_LONG + ).show() + restartApp() + } - WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { - Toast.makeText( - context, - context.resources.getString(R.string.nc_common_error_sorry), - Toast.LENGTH_LONG - ).show() - Log.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId) - restartApp() - } + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId) + restartApp() + } - else -> {} + else -> {} + } } - } + } } private fun restartApp() { @@ -1434,7 +1437,7 @@ class ConversationsListActivity : BaseActivity() { } } - if (resources!!.getBoolean(R.bool.multiaccount_support) && userManager.users.blockingGet().size > 1) { + if (resources!!.getBoolean(R.bool.multiaccount_support) && runBlocking { userManager.getUsers() }.size > 1) { dialogBuilder.setNegativeButton(R.string.nc_switch_account) { _, _ -> showChooseAccountDialog() } @@ -1475,7 +1478,7 @@ class ConversationsListActivity : BaseActivity() { deleteUserAndRestartApp() } - if (resources!!.getBoolean(R.bool.multiaccount_support) && userManager.users.blockingGet().size > 1) { + if (resources!!.getBoolean(R.bool.multiaccount_support) && runBlocking { userManager.getUsers() }.size > 1) { dialogBuilder.setNegativeButton(R.string.nc_switch_account) { _, _ -> showChooseAccountDialog() } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt index 31547455b0e..c192a67f4de 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -401,11 +401,13 @@ class ConversationsListViewModel @Inject constructor( _federationInvitationHintVisible.value = false _showAvatarBadge.value = false - userManager.users.blockingGet()?.forEach { - invitationsRepository.fetchInvitations(it) - .subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(FederatedInvitationsObserver()) + viewModelScope.launch { + userManager.getUsers().forEach { + invitationsRepository.fetchInvitations(it) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(FederatedInvitationsObserver()) + } } } diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt index 8b83c20e613..27f7019a1cc 100644 --- a/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt @@ -15,59 +15,57 @@ import androidx.room.Query import androidx.room.Update import com.nextcloud.talk.data.user.model.UserEntity import com.nextcloud.talk.models.json.push.PushConfigurationState -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single +import kotlinx.coroutines.flow.Flow @Dao @Suppress("TooManyFunctions") -abstract class UsersDao { +interface UsersDao { // get active user. ORDER BY/LIMIT make this deterministic if more than one row is ever // marked current=1 (e.g. a duplicate-account row left over from a past bug), instead of // relying on whatever order an unordered full-table scan happens to return. @Query("SELECT * FROM User where current = 1 ORDER BY id DESC LIMIT 1") - abstract fun getActiveUser(): Maybe + suspend fun getActiveUser(): UserEntity? // get active user @Query("SELECT * FROM User where current = 1 ORDER BY id DESC LIMIT 1") - abstract fun getActiveUserObservable(): Observable + fun getActiveUserFlow(): Flow @Query("SELECT * FROM User where current = 1 ORDER BY id DESC LIMIT 1") - abstract fun getActiveUserSynchronously(): UserEntity? + fun getActiveUserSynchronously(): UserEntity? @Delete - abstract fun deleteUser(user: UserEntity): Int + suspend fun deleteUser(user: UserEntity): Int @Update - abstract fun updateUser(user: UserEntity): Int + suspend fun updateUser(user: UserEntity): Int @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveUser(user: UserEntity): Long + suspend fun saveUser(user: UserEntity): Long @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveUsers(vararg users: UserEntity): List + suspend fun saveUsers(vararg users: UserEntity): List // get all users not scheduled for deletion @Query("SELECT * FROM User where scheduledForDeletion != 1") - abstract fun getUsers(): Single> + suspend fun getUsers(): List @Query("SELECT * FROM User where id = :id") - abstract fun getUserWithId(id: Long): Maybe + suspend fun getUserWithId(id: Long): UserEntity? @Query("SELECT * FROM User where id = :id AND scheduledForDeletion != 1") - abstract fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe + suspend fun getUserWithIdNotScheduledForDeletion(id: Long): UserEntity? @Query("SELECT * FROM User where userId = :userId") - abstract fun getUserWithUserId(userId: String): Maybe + suspend fun getUserWithUserId(userId: String): UserEntity? @Query("SELECT * FROM User where scheduledForDeletion = 1") - abstract fun getUsersScheduledForDeletion(): Single> + suspend fun getUsersScheduledForDeletion(): List @Query("SELECT * FROM User where scheduledForDeletion = 0") - abstract fun getUsersNotScheduledForDeletion(): Single> + suspend fun getUsersNotScheduledForDeletion(): List @Query("SELECT * FROM User WHERE username = :username AND baseUrl = :server") - abstract fun getUserWithUsernameAndServer(username: String, server: String): Maybe + suspend fun getUserWithUsernameAndServer(username: String, server: String): UserEntity? @Query( "UPDATE User SET current = CASE " + @@ -75,10 +73,10 @@ abstract class UsersDao { "WHEN id != :id THEN 0 " + "END" ) - abstract fun setUserAsActiveWithId(id: Long): Int + suspend fun setUserAsActiveWithId(id: Long): Int @Query("Update User SET pushConfigurationState = :state WHERE id == :id") - abstract fun updatePushState(id: Long, state: PushConfigurationState): Single + suspend fun updatePushState(id: Long, state: PushConfigurationState): Int companion object { const val TAG = "UsersDao" diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt index 4889b5cb74b..ce15a57077e 100644 --- a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt @@ -9,24 +9,22 @@ package com.nextcloud.talk.data.user import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.push.PushConfigurationState -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single +import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface UsersRepository { - fun getActiveUser(): Maybe - fun getActiveUserObservable(): Observable - fun getUsers(): Single> - fun getUserWithId(id: Long): Maybe - fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe - fun getUserWithUserId(userId: String): Maybe - fun getUsersScheduledForDeletion(): Single> - fun getUsersNotScheduledForDeletion(): Single> - fun getUserWithUsernameAndServer(username: String, server: String): Maybe - fun updateUser(user: User): Int - fun insertUser(user: User): Long - fun setUserAsActiveWithId(id: Long): Single - fun deleteUser(user: User): Int - fun updatePushState(id: Long, state: PushConfigurationState): Single + suspend fun getActiveUser(): User? + fun getActiveUserFlow(): Flow + suspend fun getUsers(): List + suspend fun getUserWithId(id: Long): User? + suspend fun getUserWithIdNotScheduledForDeletion(id: Long): User? + suspend fun getUserWithUserId(userId: String): User? + suspend fun getUsersScheduledForDeletion(): List + suspend fun getUsersNotScheduledForDeletion(): List + suspend fun getUserWithUsernameAndServer(username: String, server: String): User? + suspend fun updateUser(user: User): Int + suspend fun insertUser(user: User): Long + suspend fun setUserAsActiveWithId(id: Long): Boolean + suspend fun deleteUser(user: User): Int + suspend fun updatePushState(id: Long, state: PushConfigurationState): Int } diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt index e3a835f0a3e..5a53c3bbf8c 100644 --- a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt @@ -10,73 +10,55 @@ package com.nextcloud.talk.data.user import android.util.Log import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.json.push.PushConfigurationState -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @Suppress("TooManyFunctions") class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository { - override fun getActiveUser(): Maybe { - val user = usersDao.getActiveUser() - .map { - setUserAsActiveWithId(it.id) - UserMapper.toModel(it)!! - } - return user + override suspend fun getActiveUser(): User? { + val entity = usersDao.getActiveUser() ?: return null + setUserAsActiveWithId(entity.id) + return UserMapper.toModel(entity) } - override fun getActiveUserObservable(): Observable = - usersDao.getActiveUserObservable().map { + override fun getActiveUserFlow(): Flow = + usersDao.getActiveUserFlow().map { UserMapper.toModel(it) } - override fun getUsers(): Single> = usersDao.getUsers().map { UserMapper.toModel(it) } + override suspend fun getUsers(): List = UserMapper.toModel(usersDao.getUsers()) - override fun getUserWithId(id: Long): Maybe = usersDao.getUserWithId(id).map { UserMapper.toModel(it) } + override suspend fun getUserWithId(id: Long): User? = UserMapper.toModel(usersDao.getUserWithId(id)) - override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe = - usersDao.getUserWithIdNotScheduledForDeletion(id).map { - UserMapper.toModel(it) - } + override suspend fun getUserWithIdNotScheduledForDeletion(id: Long): User? = + UserMapper.toModel(usersDao.getUserWithIdNotScheduledForDeletion(id)) - override fun getUserWithUserId(userId: String): Maybe = - usersDao.getUserWithUserId(userId).map { - UserMapper.toModel(it) - } + override suspend fun getUserWithUserId(userId: String): User? = + UserMapper.toModel(usersDao.getUserWithUserId(userId)) - override fun getUsersScheduledForDeletion(): Single> = - usersDao.getUsersScheduledForDeletion().map { - UserMapper.toModel(it) - } + override suspend fun getUsersScheduledForDeletion(): List = + UserMapper.toModel(usersDao.getUsersScheduledForDeletion()) - override fun getUsersNotScheduledForDeletion(): Single> = - usersDao.getUsersNotScheduledForDeletion().map { - UserMapper.toModel(it) - } + override suspend fun getUsersNotScheduledForDeletion(): List = + UserMapper.toModel(usersDao.getUsersNotScheduledForDeletion()) - override fun getUserWithUsernameAndServer(username: String, server: String): Maybe = - usersDao.getUserWithUsernameAndServer(username, server).map { - UserMapper.toModel(it) - } + override suspend fun getUserWithUsernameAndServer(username: String, server: String): User? = + UserMapper.toModel(usersDao.getUserWithUsernameAndServer(username, server)) - override fun updateUser(user: User): Int = usersDao.updateUser(UserMapper.toEntity(user)) + override suspend fun updateUser(user: User): Int = usersDao.updateUser(UserMapper.toEntity(user)) - override fun insertUser(user: User): Long = usersDao.saveUser(UserMapper.toEntity(user)) + override suspend fun insertUser(user: User): Long = usersDao.saveUser(UserMapper.toEntity(user)) - override fun setUserAsActiveWithId(id: Long): Single { + override suspend fun setUserAsActiveWithId(id: Long): Boolean { val amountUpdated = usersDao.setUserAsActiveWithId(id) Log.d(TAG, "setUserAsActiveWithId. amountUpdated: $amountUpdated") - return if (amountUpdated > 0) { - Single.just(true) - } else { - Single.just(false) - } + return amountUpdated > 0 } - override fun deleteUser(user: User): Int = usersDao.deleteUser(UserMapper.toEntity(user)) + override suspend fun deleteUser(user: User): Int = usersDao.deleteUser(UserMapper.toEntity(user)) - override fun updatePushState(id: Long, state: PushConfigurationState): Single = + override suspend fun updatePushState(id: Long, state: PushConfigurationState): Int = usersDao.updatePushState(id, state) companion object { diff --git a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt index 674fe18a9fe..2bd9ba6c41a 100644 --- a/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt +++ b/app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisElement.kt @@ -25,6 +25,7 @@ import com.nextcloud.talk.utils.UnifiedPushUtils import com.nextcloud.talk.utils.UserIdUtils import com.nextcloud.talk.utils.power.PowerManagerUtils import com.nextcloud.talk.utils.preferences.AppPreferences +import kotlinx.coroutines.runBlocking import org.unifiedpush.android.connector.UnifiedPush sealed class DiagnosisElement { @@ -65,7 +66,7 @@ fun buildDiagnosisElements( val isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable val nUnifiedPushServices = UnifiedPushUtils.getExternalDistributors(context).size val offerUnifiedPush = try { - nUnifiedPushServices > 0 && userManager.users.blockingGet().all { it.hasWebPushCapability } + nUnifiedPushServices > 0 && runBlocking { userManager.getUsers() }.all { it.hasWebPushCapability } } catch (_: Exception) { false } @@ -184,7 +185,7 @@ fun buildDiagnosisElements( try { addEntry( context.getString(R.string.nc_diagnosis_app_users_amount), - userManager.users.blockingGet().size.toString() + runBlocking { userManager.getUsers() }.size.toString() ) } catch (_: Exception) { } diff --git a/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt b/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt index ffa1b12592b..fe74b4677c1 100644 --- a/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt @@ -23,6 +23,7 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -44,8 +45,13 @@ class InvitationsViewModel @Inject constructor(private val repository: Invitatio open class GetInvitationsErrorState(val error: Exception) : ViewState open class GetInvitationsSuccessState(val invitations: List) : ViewState - private val _getInvitationsViewState = MutableStateFlow(GetInvitationsStartState) - val getInvitationsViewState: StateFlow = _getInvitationsViewState + /** + * Keyed by [User.id], since [getInvitations] is called once per saved account (the account + * switcher checks every account for pending invitations) and each account's result must be + * attributed back to that account rather than overwriting a single shared value. + */ + private val _invitationsStateByUser = MutableStateFlow>(emptyMap()) + val invitationsStateByUser: StateFlow> = _invitationsStateByUser object InvitationActionStartState : ViewState object InvitationActionErrorState : ViewState @@ -67,17 +73,19 @@ class InvitationsViewModel @Inject constructor(private val repository: Invitatio @Suppress("TooGenericExceptionCaught") fun getInvitations(user: User) { + val userId = user.id ?: return viewModelScope.launch { - try { + val state = try { val invitationsModel = repository.getInvitations(user) if (invitationsModel.invitations.isEmpty()) { - _getInvitationsViewState.value = GetInvitationsEmptyState + GetInvitationsEmptyState } else { - _getInvitationsViewState.value = GetInvitationsSuccessState(invitationsModel.invitations) + GetInvitationsSuccessState(invitationsModel.invitations) } } catch (e: Exception) { - _getInvitationsViewState.value = GetInvitationsErrorState(e) + GetInvitationsErrorState(e) } + _invitationsStateByUser.update { it + (userId to state) } } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.kt index 3e3bc44a090..5843fdc59be 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.kt @@ -81,7 +81,7 @@ class ChatMessageCatchUpWorker(context: Context, workerParams: WorkerParameters) } private suspend fun catchUpRoom(userId: Long, roomToken: String, threadId: Long?): Result { - val user = userManager.getUserWithId(userId).blockingGet() + val user = userManager.getUserWithIdSuspend(userId) val credentials = user?.let { ApiUtils.getCredentials(it.username, it.token) } if (user == null || credentials == null) { Log.e(TAG, "No user or credentials found for user id $userId, dropping message catch-up") diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 4d751a79e12..dc15ad79784 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -251,7 +251,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor @Suppress("LongMethod", "TooGenericExceptionCaught") private fun handleCallPushMessage() { - val userBeingCalled = userManager.getUserWithId(user.id!!).blockingGet() + val userBeingCalled = runBlocking { userManager.getUserWithIdSuspend(user.id!!) } fun createBundle(conversation: ConversationModel): Bundle { val bundle = Bundle() @@ -396,13 +396,13 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor } val conversation = try { - runBlocking { chatNetworkDataSource?.getRoom(userBeingCalled, roomToken = pushMessage.id!!) } + runBlocking { chatNetworkDataSource?.getRoom(userBeingCalled!!, roomToken = pushMessage.id!!) } } catch (e: Exception) { Log.e(TAG, "Failed to get room", e) null } - if (conversation != null && userManager.setUserAsActive(userBeingCalled!!).blockingGet()) { + if (conversation != null && runBlocking { userManager.setUserAsActiveSuspend(userBeingCalled!!) }) { if (CapabilitiesUtil.isCallEndToEndEncryptionEnabled(userBeingCalled?.capabilities?.spreedCapability)) { showEndToEndEncryptionUnsupportedNotification(conversation) } else { @@ -440,7 +440,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor private fun initFromCleartextSubject(inputData: Data): Boolean { val subject = inputData.getString(BundleKeys.KEY_NOTIFICATION_CLEARTEXT_SUBJECT) val id = inputData.getLong(BundleKeys.KEY_NOTIFICATION_USER_ID, -1) - user = userManager.getUserWithId(id).blockingGet() + user = runBlocking { userManager.getUserWithIdSuspend(id) }!! pushMessage = LoganSquare.parse(subject, DecryptedPushMessage::class.java) return true } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt index 177e6ffb74e..772694a804c 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt @@ -29,6 +29,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.preferences.AppPreferences import io.reactivex.Observable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import okhttp3.CookieJar import okhttp3.OkHttpClient @@ -119,7 +120,7 @@ class PushRegistrationWorker(context: Context, workerParams: WorkerParameters) : */ @SuppressLint("CheckResult") private fun webPushActivationWork(id: Long, activationToken: String) { - val user = userManager.getUserWithId(id).blockingGet() + val user = runBlocking { userManager.getUserWithIdSuspend(id) }!! activateWebPushForAccount(user, activationToken) .flatMap { res -> if (res) { @@ -144,7 +145,7 @@ class PushRegistrationWorker(context: Context, workerParams: WorkerParameters) : @SuppressLint("CheckResult") private fun webPushWork(id: Long, pushEndpoint: PushEndpoint) { preferences.unifiedPushLatestEndpoint = System.currentTimeMillis() - val user = userManager.getUserWithId(id).blockingGet() + val user = runBlocking { userManager.getUserWithIdSuspend(id) }!! registerWebPushForAccount(user, pushEndpoint) .map { (user, res) -> if (res) { @@ -168,18 +169,17 @@ class PushRegistrationWorker(context: Context, workerParams: WorkerParameters) : */ @SuppressLint("CheckResult") private fun webPushUnregistrationWork(id: Long) { - userManager.getUserWithId(id).map { user -> - unregisterWebPushForAccount(user) - .toList() - .subscribeOn(Schedulers.io()) - .subscribe { _, e -> - e?.let { - Log.e(TAG, "An error occurred while unregistering for web push", e) - } ?: { - Log.d(TAG, "${user.userId} unregistered from web push") - } + val user = runBlocking { userManager.getUserWithIdSuspend(id) } ?: return + unregisterWebPushForAccount(user) + .toList() + .subscribeOn(Schedulers.io()) + .subscribe { _, e -> + e?.let { + Log.e(TAG, "An error occurred while unregistering for web push", e) + } ?: { + Log.d(TAG, "${user.userId} unregistered from web push") } - } + } } /** @@ -187,7 +187,7 @@ class PushRegistrationWorker(context: Context, workerParams: WorkerParameters) : */ @SuppressLint("CheckResult") private fun unifiedPushWork() { - val obs = userManager.users.blockingGet().map { user -> + val obs = runBlocking { userManager.getUsers() }.map { user -> registerUnifiedPushForAccount(user) } Observable.merge(obs) @@ -206,7 +206,7 @@ class PushRegistrationWorker(context: Context, workerParams: WorkerParameters) : */ @SuppressLint("CheckResult") private fun proxyPushWork() { - val obs = userManager.users.blockingGet().mapNotNull { user -> + val obs = runBlocking { userManager.getUsers() }.mapNotNull { user -> if (user.userId == null || user.baseUrl == null) { Log.w(TAG, "Null userId or baseUrl (userId=${user.userId}, baseUrl=${user.baseUrl}") return@mapNotNull null @@ -273,7 +273,7 @@ class PushRegistrationWorker(context: Context, workerParams: WorkerParameters) : */ @SuppressLint("CheckResult") private fun enqueueNotifUnifiedPushDisabled() { - val user = userManager.users.blockingGet().first() + val user = runBlocking { userManager.getUsers() }.first() Log.d(TAG, "Sending warning notification with ${user.userId}") val notif = hashMapOf( "subject" to "UnifiedPush disabled", diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ReadMarkerSyncWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ReadMarkerSyncWorker.kt index 5f40d037dc8..8888c129098 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ReadMarkerSyncWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/ReadMarkerSyncWorker.kt @@ -72,8 +72,8 @@ class ReadMarkerSyncWorker(context: Context, workerParams: WorkerParameters) : } } - private fun sendReadMarker(userId: Long, roomToken: String, lastReadMessage: Int): Result { - val user = userManager.getUserWithId(userId).blockingGet() + private suspend fun sendReadMarker(userId: Long, roomToken: String, lastReadMessage: Int): Result { + val user = userManager.getUserWithIdSuspend(userId) val credentials = user?.let { ApiUtils.getCredentials(it.username, it.token) } if (user == null || credentials == null) { Log.e(TAG, "No user or credentials found for user id $userId, dropping read marker sync") diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt index ed9e5596c77..9b0c47b32f9 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt @@ -29,6 +29,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.runBlocking import retrofit2.HttpException import javax.inject.Inject @@ -94,7 +95,7 @@ class ShareOperationWorker(context: Context, workerParams: WorkerParameters) : W metaData = data.getString(KEY_META_DATA) data.getStringArray(KEY_FILE_PATHS)?.let { filesArray.addAll(it.toList()) } - val operationsUser = userManager.getUserWithId(userId).blockingGet() + val operationsUser = runBlocking { userManager.getUserWithIdSuspend(userId) }!! baseUrl = operationsUser.baseUrl credentials = ApiUtils.getCredentials(operationsUser.username, operationsUser.token)!! } diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt index 0620602e3ac..9aacf541181 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt @@ -41,6 +41,7 @@ import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.runBlocking import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -74,7 +75,7 @@ class DirectReplyReceiver : BroadcastReceiver() { roomToken = intent.getStringExtra(KEY_ROOM_TOKEN) val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!) - currentUser = userManager.getUserWithId(id).blockingGet() + currentUser = runBlocking { userManager.getUserWithIdSuspend(id) }!! replyMessage = getMessageText(intent) sendDirectReply() diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt index 85639b7ea68..c8cf4e657a2 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt @@ -26,6 +26,7 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.runBlocking import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -58,7 +59,7 @@ class DismissRecordingAvailableReceiver : BroadcastReceiver() { link = intent.getStringExtra(BundleKeys.KEY_DISMISS_RECORDING_URL) val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!) - currentUser = userManager.getUserWithId(id).blockingGet() + currentUser = runBlocking { userManager.getUserWithIdSuspend(id) }!! dismissNcRecordingAvailableNotification() } diff --git a/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt index 7b1af1c1536..ac032e7a4a0 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt @@ -28,6 +28,7 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.runBlocking import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -62,7 +63,7 @@ class MarkAsReadReceiver : BroadcastReceiver() { messageId = intent.getIntExtra(KEY_MESSAGE_ID, 0) val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!) - currentUser = userManager.getUserWithId(id).blockingGet() + currentUser = runBlocking { userManager.getUserWithIdSuspend(id) }!! markAsRead() } diff --git a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt index be1bd8d852f..96aba7fdd2e 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt +++ b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt @@ -29,6 +29,7 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.runBlocking import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -58,7 +59,7 @@ class ShareRecordingToChatReceiver : BroadcastReceiver() { link = intent.getStringExtra(BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL) val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!) - currentUser = userManager.getUserWithId(id).blockingGet() + currentUser = runBlocking { userManager.getUserWithIdSuspend(id) }!! shareRecordingToChat() } diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt index d2919b72dd5..4a857e0ecf9 100644 --- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt @@ -107,6 +107,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody @@ -362,7 +363,7 @@ class SettingsActivity : private fun showUnifiedPushToggle(): Boolean = UnifiedPushUtils.getExternalDistributors(this).isNotEmpty() && - userManager.users.blockingGet().all { it.hasWebPushCapability } + runBlocking { userManager.getUsers() }.all { it.hasWebPushCapability } private fun setupUnifiedPushSettings() { // If any user doesn't support web push, or there is no UnifiedPush @@ -775,7 +776,9 @@ class SettingsActivity : } Log.d(TAG, "host: $host and port: $port") currentUser!!.clientCertificate = finalAlias - userManager.updateOrCreateUser(currentUser!!) + lifecycleScope.launch { + userManager.updateOrCreateUserSuspend(currentUser!!) + } }, arrayOf("RSA", "EC"), null, @@ -867,44 +870,46 @@ class SettingsActivity : } } - @SuppressLint("CheckResult", "StringFormatInvalid") + @SuppressLint("StringFormatInvalid") private fun removeCurrentAccount() { - userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() - val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) - .setExpeditedIfSupported() - .build() - WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) - - WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) - .observeForever { workInfo: WorkInfo? -> + lifecycleScope.launch { + userManager.scheduleUserForDeletionWithIdSuspend(currentUser!!.id!!) + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) + .setExpeditedIfSupported() + .build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo? -> + + when (workInfo?.state) { + WorkInfo.State.SUCCEEDED -> { + val text = String.format( + context.resources.getString(R.string.nc_deleted_user), + currentUser!!.displayName + ) + Toast.makeText( + context, + text, + Toast.LENGTH_LONG + ).show() + restartApp() + } - when (workInfo?.state) { - WorkInfo.State.SUCCEEDED -> { - val text = String.format( - context.resources.getString(R.string.nc_deleted_user), - currentUser!!.displayName - ) - Toast.makeText( - context, - text, - Toast.LENGTH_LONG - ).show() - restartApp() - } + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId) + restartApp() + } - WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { - Toast.makeText( - context, - context.resources.getString(R.string.nc_common_error_sorry), - Toast.LENGTH_LONG - ).show() - Log.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId) - restartApp() + else -> {} } - - else -> {} } - } + } } private fun restartApp() { @@ -1089,7 +1094,9 @@ class SettingsActivity : } if ((!TextUtils.isEmpty(displayName) && !(displayName == currentUser!!.displayName))) { currentUser!!.displayName = displayName - userManager.updateOrCreateUser(currentUser!!) + lifecycleScope.launch { + userManager.updateOrCreateUserSuspend(currentUser!!) + } binding.nameText.text = currentUser!!.displayName } }, diff --git a/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt b/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt index 943804d14d9..3ecd1f0c705 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chooseaccount/ChooseAccountShareToViewModel.kt @@ -9,6 +9,7 @@ package com.nextcloud.talk.ui.chooseaccount import android.util.Log import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.ui.chooseaccount.model.ChooseAccountShareToViewState import com.nextcloud.talk.ui.chooseaccount.model.LoadUsersStartStateChooseAccountShareTo @@ -16,65 +17,57 @@ import com.nextcloud.talk.ui.chooseaccount.model.LoadUsersSuccessStateChooseAcco import com.nextcloud.talk.ui.chooseaccount.model.SwitchUserErrorStateChooseAccountShareTo import com.nextcloud.talk.ui.chooseaccount.model.SwitchUserSuccessStateChooseAccountShareTo import com.nextcloud.talk.users.UserManager -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.schedulers.Schedulers +import com.nextcloud.talk.utils.database.user.CurrentUserProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import javax.inject.Inject -class ChooseAccountShareToViewModel @Inject constructor(private val userManager: UserManager) : ViewModel() { +class ChooseAccountShareToViewModel @Inject constructor( + private val userManager: UserManager, + currentUserProvider: CurrentUserProvider +) : ViewModel() { - val currentUser: User? = userManager.currentUser.blockingGet() + val currentUser: User? = runBlocking { currentUserProvider.getCurrentUser() }.getOrNull() private val _chooseAccountShareToViewState: MutableStateFlow = MutableStateFlow(LoadUsersStartStateChooseAccountShareTo) val chooseAccountShareToViewState: StateFlow = _chooseAccountShareToViewState.asStateFlow() - private val disposables = CompositeDisposable() - + @Suppress("TooGenericExceptionCaught") fun loadUsers() { _chooseAccountShareToViewState.value = LoadUsersStartStateChooseAccountShareTo - disposables.add( - userManager.users - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { users -> - _chooseAccountShareToViewState.value = - LoadUsersSuccessStateChooseAccountShareTo(users.filter { !it.current }) - }, - { e -> - Log.e(TAG, "Error loading users", e) - _chooseAccountShareToViewState.value = LoadUsersSuccessStateChooseAccountShareTo(emptyList()) - } - ) - ) + viewModelScope.launch { + try { + val users = userManager.getUsers() + _chooseAccountShareToViewState.value = + LoadUsersSuccessStateChooseAccountShareTo(users.filter { !it.current }) + } catch (e: Exception) { + Log.e(TAG, "Error loading users", e) + _chooseAccountShareToViewState.value = LoadUsersSuccessStateChooseAccountShareTo(emptyList()) + } + } } + @Suppress("TooGenericExceptionCaught") fun switchToUser(user: User) { - disposables.add( - userManager.setUserAsActive(user) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()).subscribe({ success -> - _chooseAccountShareToViewState.value = - if (success) { - SwitchUserSuccessStateChooseAccountShareTo - } else { - SwitchUserErrorStateChooseAccountShareTo - } - }, { e -> - Log.e(TAG, "Error switching user", e) - _chooseAccountShareToViewState.value = SwitchUserErrorStateChooseAccountShareTo - }) - ) - } - - override fun onCleared() { - disposables.dispose() - super.onCleared() + viewModelScope.launch { + try { + val success = userManager.setUserAsActiveSuspend(user) + _chooseAccountShareToViewState.value = + if (success) { + SwitchUserSuccessStateChooseAccountShareTo + } else { + SwitchUserErrorStateChooseAccountShareTo + } + } catch (e: Exception) { + Log.e(TAG, "Error switching user", e) + _chooseAccountShareToViewState.value = SwitchUserErrorStateChooseAccountShareTo + } + } } companion object { diff --git a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt index 8a785130a1b..8549616ab56 100644 --- a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt +++ b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt @@ -21,32 +21,53 @@ import io.reactivex.Observable import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.rx2.rxMaybe +import kotlinx.coroutines.rx2.rxSingle @Suppress("TooManyFunctions") class UserManager internal constructor(private val userRepository: UsersRepository) { + + private val managerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + @Deprecated("Use suspend fun getUsers() instead") val users: Single> - get() = userRepository.getUsers() + get() = rxSingle { getUsers() } + suspend fun getUsers(): List = userRepository.getUsers() + + @Deprecated("Use suspend fun getUsersScheduledForDeletion() instead") val usersScheduledForDeletion: Single> - get() = userRepository.getUsersScheduledForDeletion() + get() = rxSingle { getUsersScheduledForDeletion() } + + suspend fun getUsersScheduledForDeletion(): List = userRepository.getUsersScheduledForDeletion() + /** + * @deprecated coroutine-native code should use [com.nextcloud.talk.utils.database.user.CurrentUserProvider] + * instead. + */ + @Deprecated("Use CurrentUserProvider.getCurrentUser() instead") val currentUser: Maybe - get() { - return userRepository.getActiveUser() - .switchIfEmpty(Maybe.defer { getAnyUserAndSetAsActive() }) - } + get() = rxMaybe { getCurrentUserOrAny() } + + private suspend fun getCurrentUserOrAny(): User? = userRepository.getActiveUser() ?: getAnyUserAndSetAsActive() /** - * Backed by [activeUserSubject] rather than [UsersRepository.getActiveUserObservable] directly, so that + * Backed by [activeUserSubject] rather than [UsersRepository.getActiveUserFlow] directly, so that * [setUserAsActive] can push the newly-active user out synchronously the moment it succeeds, instead of * consumers having to wait for Room's invalidation-tracker round trip to notice the DB write and re-query. * That round trip is asynchronous and was racing against code (e.g. AccountVerificationActivity. * proceedWithLogin()) that both changes the active user and immediately acts as if every observer already * knows about it - e.g. launching a screen for the new user before its avatar/data had actually updated. - * Room's own observable is still relied on underneath to seed this and to catch any change to the `current` - * flag that doesn't go through [setUserAsActive]. + * Room's own [UsersRepository.getActiveUserFlow] is still relied on underneath to seed this and to catch + * any change to the `current` flag that doesn't go through [setUserAsActive]. * * RxJava-based for CurrentUserProviderOld, the still-used but deprecated consumer. Coroutine-based code * should prefer [currentUserFlow] instead, which is updated at the exact same point and needs no RxJava @@ -64,50 +85,67 @@ class UserManager internal constructor(private val userRepository: UsersReposito private val activeUserSubject: Subject by lazy { val subject = BehaviorSubject.create().toSerialized() - userRepository.getActiveUserObservable().subscribe(subject::onNext) { } + managerScope.launch { + userRepository.getActiveUserFlow().filterNotNull().collect(subject::onNext) + } subject } private val activeUserStateFlow: MutableStateFlow by lazy { val flow = MutableStateFlow(null) - userRepository.getActiveUserObservable().subscribe({ flow.value = it }) { } + managerScope.launch { + userRepository.getActiveUserFlow().collect { flow.value = it } + } flow } - fun deleteUser(internalId: Long): Int = - userRepository.deleteUser(userRepository.getUserWithId(internalId).blockingGet()) + @Deprecated("Use suspend fun deleteUser(internalId: Long) instead") + fun deleteUser(internalId: Long): Int = runBlocking { deleteUserSuspend(internalId) } + + suspend fun deleteUserSuspend(internalId: Long): Int { + val user = userRepository.getUserWithId(internalId) ?: return 0 + return userRepository.deleteUser(user) + } + + @Deprecated("Use suspend fun getUserWithId(id: Long) instead") + fun getUserWithId(id: Long): Maybe = rxMaybe { getUserWithIdSuspend(id) } - fun getUserWithId(id: Long): Maybe = userRepository.getUserWithId(id) + suspend fun getUserWithIdSuspend(id: Long): User? = userRepository.getUserWithId(id) + @Deprecated("Use suspend fun checkIfUserIsScheduledForDeletion(username, server) instead") fun checkIfUserIsScheduledForDeletion(username: String, server: String): Single = - userRepository - .getUserWithUsernameAndServer(username, server) - .map { it.scheduledForDeletion } - .switchIfEmpty(Single.just(false)) + rxSingle { checkIfUserIsScheduledForDeletionSuspend(username, server) } - fun getUserWithInternalId(id: Long): Maybe = userRepository.getUserWithIdNotScheduledForDeletion(id) + suspend fun checkIfUserIsScheduledForDeletionSuspend(username: String, server: String): Boolean = + userRepository.getUserWithUsernameAndServer(username, server)?.scheduledForDeletion ?: false + @Deprecated("Use suspend fun getUserWithInternalId(id: Long) instead") + fun getUserWithInternalId(id: Long): Maybe = rxMaybe { getUserWithInternalIdSuspend(id) } + + suspend fun getUserWithInternalIdSuspend(id: Long): User? = userRepository.getUserWithIdNotScheduledForDeletion(id) + + @Deprecated("Use suspend fun checkIfUserExists(username, server) instead") fun checkIfUserExists(username: String, server: String): Single = - userRepository - .getUserWithUsernameAndServer(username, server) - .map { true } - .switchIfEmpty(Single.just(false)) + rxSingle { checkIfUserExistsSuspend(username, server) } + + suspend fun checkIfUserExistsSuspend(username: String, server: String): Boolean = + userRepository.getUserWithUsernameAndServer(username, server) != null /** * Don't ask * * @return `true` if the user was updated **AND** there is another user to set as active, `false` otherwise */ - fun scheduleUserForDeletionWithId(id: Long): Single = - userRepository.getUserWithId(id) - .map { user -> - user.scheduledForDeletion = true - user.current = false - userRepository.updateUser(user) - } - .flatMap { getAnyUserAndSetAsActive() } - .map { true } - .switchIfEmpty(Single.just(false)) + @Deprecated("Use suspend fun scheduleUserForDeletionWithId(id: Long) instead") + fun scheduleUserForDeletionWithId(id: Long): Single = rxSingle { scheduleUserForDeletionWithIdSuspend(id) } + + suspend fun scheduleUserForDeletionWithIdSuspend(id: Long): Boolean { + val user = userRepository.getUserWithId(id) ?: return false + user.scheduledForDeletion = true + user.current = false + userRepository.updateUser(user) + return getAnyUserAndSetAsActive() != null + } /** * If there is more than one local User row for the same username+baseUrl (e.g. reusing the @@ -124,120 +162,114 @@ class UserManager internal constructor(private val userRepository: UsersReposito * * @return the number of duplicate rows scheduled for deletion */ - fun scheduleDuplicateAccountsForDeletion(): Single = - Single.zip( - users, - userRepository.getActiveUser().map { it.id }.toSingle(NO_ACTIVE_USER_ID) - ) { allUsers, activeUserId -> - val duplicateGroups = allUsers - .filter { !it.username.isNullOrEmpty() && !it.baseUrl.isNullOrEmpty() } - .groupBy { it.username to it.baseUrl } - .values - .filter { it.size > 1 } - - var scheduledCount = 0 - duplicateGroups.forEach { duplicates -> - val userToKeep = duplicates.firstOrNull { it.id == activeUserId } - ?: duplicates.firstOrNull { it.current } - ?: duplicates.minByOrNull { it.id ?: Long.MAX_VALUE } - duplicates - .filter { it.id != userToKeep?.id } - .forEach { duplicate -> - duplicate.scheduledForDeletion = true - userRepository.updateUser(duplicate) - scheduledCount++ - } - } - scheduledCount - } + @Deprecated("Use suspend fun scheduleDuplicateAccountsForDeletion() instead") + fun scheduleDuplicateAccountsForDeletion(): Single = rxSingle { scheduleDuplicateAccountsForDeletionSuspend() } - private fun getAnyUserAndSetAsActive(): Maybe { - val results = userRepository.getUsersNotScheduledForDeletion() + suspend fun scheduleDuplicateAccountsForDeletionSuspend(): Int { + val allUsers = getUsers() + val activeUserId = userRepository.getActiveUser()?.id ?: NO_ACTIVE_USER_ID - return results - .flatMapMaybe { - if (it.isNotEmpty()) { - val user = it.first() - if (setUserAsActive(user).blockingGet()) { - userRepository.getActiveUser() - } else { - Maybe.empty() - } - } else { - Maybe.empty() + val duplicateGroups = allUsers + .filter { !it.username.isNullOrEmpty() && !it.baseUrl.isNullOrEmpty() } + .groupBy { it.username to it.baseUrl } + .values + .filter { it.size > 1 } + + var scheduledCount = 0 + duplicateGroups.forEach { duplicates -> + val userToKeep = duplicates.firstOrNull { it.id == activeUserId } + ?: duplicates.firstOrNull { it.current } + ?: duplicates.minByOrNull { it.id ?: Long.MAX_VALUE } + duplicates + .filter { it.id != userToKeep?.id } + .forEach { duplicate -> + duplicate.scheduledForDeletion = true + userRepository.updateUser(duplicate) + scheduledCount++ } - } + } + return scheduledCount } - fun updateExternalSignalingServer(id: Long, externalSignalingServer: ExternalSignalingServer): Single = - userRepository.getUserWithId(id).map { user -> - user.externalSignalingServer = externalSignalingServer - userRepository.updateUser(user) - }.toSingle() - - fun updateOrCreateUser(user: User): Single = - Single.fromCallable { - when (user.id) { - null -> userRepository.insertUser(user).toInt() - else -> userRepository.updateUser(user) - } + private suspend fun getAnyUserAndSetAsActive(): User? { + val results = userRepository.getUsersNotScheduledForDeletion() + if (results.isEmpty()) { + return null } + val user = results.first() + return if (setUserAsActiveSuspend(user)) { + userRepository.getActiveUser() + } else { + null + } + } - fun saveUser(user: User): Single = - Single.fromCallable { - userRepository.updateUser(user) + @Deprecated("Use suspend fun updateExternalSignalingServer(id, externalSignalingServer) instead") + fun updateExternalSignalingServer(id: Long, externalSignalingServer: ExternalSignalingServer): Single = + rxSingle { updateExternalSignalingServerSuspend(id, externalSignalingServer) } + + suspend fun updateExternalSignalingServerSuspend(id: Long, externalSignalingServer: ExternalSignalingServer): Int { + val user = userRepository.getUserWithId(id) ?: throw NoSuchElementException() + user.externalSignalingServer = externalSignalingServer + return userRepository.updateUser(user) + } + + @Deprecated("Use suspend fun updateOrCreateUser(user) instead") + fun updateOrCreateUser(user: User): Single = rxSingle { updateOrCreateUserSuspend(user) } + + suspend fun updateOrCreateUserSuspend(user: User): Int = + when (user.id) { + null -> userRepository.insertUser(user).toInt() + else -> userRepository.updateUser(user) } - fun setUserAsActive(user: User): Single { + @Deprecated("Use suspend fun saveUser(user) instead") + fun saveUser(user: User): Single = rxSingle { saveUserSuspend(user) } + + suspend fun saveUserSuspend(user: User): Int = userRepository.updateUser(user) + + @Deprecated("Use suspend fun setUserAsActive(user) instead") + fun setUserAsActive(user: User): Single = rxSingle { setUserAsActiveSuspend(user) } + + suspend fun setUserAsActiveSuspend(user: User): Boolean { Log.d(TAG, "setUserAsActive:" + user.id!!) - return userRepository.setUserAsActiveWithId(user.id!!) - .doOnSuccess { success -> - if (success) { - activeUserSubject.onNext(user) - activeUserStateFlow.value = user - } - } + val success = userRepository.setUserAsActiveWithId(user.id!!) + if (success) { + activeUserSubject.onNext(user) + activeUserStateFlow.value = user + } + return success } + @Deprecated("Use suspend fun storeProfile(username, userAttributes) instead") fun storeProfile(username: String?, userAttributes: UserAttributes): Maybe = - findUser(userAttributes) - .map { user: User? -> - when (user) { - null -> createUser( - username, - userAttributes - ) - else -> { - user.token = userAttributes.token - user.baseUrl = userAttributes.serverUrl - user.current = userAttributes.currentUser - user.userId = userAttributes.userId - user.token = userAttributes.token - user.displayName = userAttributes.displayName - user.clientCertificate = userAttributes.certificateAlias - - updateUserData( - user, - userAttributes - ) - - user - } - } - } - .switchIfEmpty(Maybe.just(createUser(username, userAttributes))) - .map { user -> - userRepository.insertUser(user) - } - .flatMap { id -> - userRepository.getUserWithId(id) + rxMaybe { storeProfileSuspend(username, userAttributes) } + + suspend fun storeProfileSuspend(username: String?, userAttributes: UserAttributes): User? { + val existingUser = findUser(userAttributes) + val user = if (existingUser != null) { + existingUser.apply { + token = userAttributes.token + baseUrl = userAttributes.serverUrl + current = userAttributes.currentUser + userId = userAttributes.userId + token = userAttributes.token + displayName = userAttributes.displayName + clientCertificate = userAttributes.certificateAlias + updateUserData(this, userAttributes) } + } else { + createUser(username, userAttributes) + } + val id = userRepository.insertUser(user) + return userRepository.getUserWithId(id) + } - private fun findUser(userAttributes: UserAttributes): Maybe = + private suspend fun findUser(userAttributes: UserAttributes): User? = if (userAttributes.id != null) { userRepository.getUserWithId(userAttributes.id) } else { - Maybe.empty() + null } private fun updateUserData(user: User, userAttributes: UserAttributes) { @@ -296,7 +328,11 @@ class UserManager internal constructor(private val userRepository: UsersReposito return user } + @Deprecated("Use suspend fun updatePushState(id, state) instead") fun updatePushState(id: Long, state: PushConfigurationState): Single = + rxSingle { updatePushStateSuspend(id, state) } + + suspend fun updatePushStateSuspend(id: Long, state: PushConfigurationState): Int = userRepository.updatePushState(id, state) companion object { diff --git a/app/src/main/java/com/nextcloud/talk/utils/RemoteWipeInterceptor.kt b/app/src/main/java/com/nextcloud/talk/utils/RemoteWipeInterceptor.kt index 9527648fe12..6f481b23900 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/RemoteWipeInterceptor.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/RemoteWipeInterceptor.kt @@ -7,7 +7,6 @@ package com.nextcloud.talk.utils -import android.annotation.SuppressLint import android.content.Context import android.util.Log import androidx.work.Data @@ -22,6 +21,7 @@ import com.nextcloud.talk.models.json.wipe.WipeCheckResponse import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat import com.nextcloud.talk.utils.ssl.TrustManager +import kotlinx.coroutines.runBlocking import okhttp3.FormBody import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -73,7 +73,7 @@ class RemoteWipeInterceptor( } private fun resolveWipeCandidate(requestUrl: String): WipeCandidate? { - val user = userManager.users.blockingGet() + val user = runBlocking { userManager.getUsers() } .firstOrNull { it.baseUrl != null && requestUrl.startsWith(it.baseUrl!!) } if (user == null) { Log.d(TAG, "No known user matches base URL of $requestUrl, ignoring") @@ -124,10 +124,9 @@ class RemoteWipeInterceptor( return wipeRequestedByServer } - @SuppressLint("CheckResult") private fun performWipe(candidate: WipeCandidate, wipeRequestedByServer: Boolean) { Log.d(TAG, "Scheduling user ${candidate.userId} for deletion") - userManager.scheduleUserForDeletionWithId(candidate.userId).blockingGet() + runBlocking { userManager.scheduleUserForDeletionWithIdSuspend(candidate.userId) } val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() var workContinuation = WorkManager.getInstance(context).beginWith(accountRemovalWork) diff --git a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt index 6e3ad48553b..1dc27b7788a 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt @@ -16,9 +16,6 @@ import com.nextcloud.talk.data.database.model.ConversationEntity import com.nextcloud.talk.data.user.UsersDao import com.nextcloud.talk.data.user.model.UserEntity import com.nextcloud.talk.models.json.push.PushConfigurationState -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf @@ -161,7 +158,7 @@ class DummyChatMessagesDaoImpl : ChatMessagesDao { override fun getNumberOfThreadReplies(internalConversationId: String, threadId: Long): Int = 0 } -class DummyUserDaoImpl : UsersDao() { +class DummyUserDaoImpl : UsersDao { private val dummyUsers = mutableListOf( UserEntity(1L, "user1_id", "user1", "server1", "1"), UserEntity(2L, "user2_id", "user2", "server1", "2"), @@ -169,28 +166,24 @@ class DummyUserDaoImpl : UsersDao() { ) private var activeUserId: Long? = 1L - override fun getActiveUser(): Maybe = - Maybe.fromCallable { - dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } - } + override suspend fun getActiveUser(): UserEntity? = + dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } - override fun getActiveUserObservable(): Observable = - Observable.fromCallable { - dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } - } + override fun getActiveUserFlow(): Flow = + flowOf(dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion }) override fun getActiveUserSynchronously(): UserEntity? = dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } - override fun deleteUser(user: UserEntity): Int { + override suspend fun deleteUser(user: UserEntity): Int { val initialSize = dummyUsers.size dummyUsers.removeIf { it.id == user.id } return initialSize - dummyUsers.size } - override fun updateUser(user: UserEntity): Int { + override suspend fun updateUser(user: UserEntity): Int { val index = dummyUsers.indexOfFirst { it.id == user.id } return if (index != -1) { dummyUsers[index] = user @@ -200,59 +193,48 @@ class DummyUserDaoImpl : UsersDao() { } } - override fun saveUser(user: UserEntity): Long { + override suspend fun saveUser(user: UserEntity): Long { val newUser = user.copy(id = dummyUsers.size + 1L) dummyUsers.add(newUser) return newUser.id } - override fun saveUsers(vararg users: UserEntity): List = users.map { saveUser(it) } + override suspend fun saveUsers(vararg users: UserEntity): List = users.map { saveUser(it) } - override fun getUsers(): Single> = Single.just(dummyUsers.filter { !it.scheduledForDeletion }) + override suspend fun getUsers(): List = dummyUsers.filter { !it.scheduledForDeletion } - override fun getUserWithId(id: Long): Maybe = Maybe.fromCallable { dummyUsers.find { it.id == id } } + override suspend fun getUserWithId(id: Long): UserEntity? = dummyUsers.find { it.id == id } - override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe = - Maybe.fromCallable { - dummyUsers.find { it.id == id && !it.scheduledForDeletion } - } + override suspend fun getUserWithIdNotScheduledForDeletion(id: Long): UserEntity? = + dummyUsers.find { it.id == id && !it.scheduledForDeletion } + + override suspend fun getUserWithUserId(userId: String): UserEntity? = dummyUsers.find { it.userId == userId } - override fun getUserWithUserId(userId: String): Maybe = - Maybe.fromCallable { - dummyUsers.find { it.userId == userId } + override suspend fun getUsersScheduledForDeletion(): List = + dummyUsers.filter { + it.scheduledForDeletion } - override fun getUsersScheduledForDeletion(): Single> = - Single.just( - dummyUsers.filter { - it.scheduledForDeletion - } - ) - - override fun getUsersNotScheduledForDeletion(): Single> = - Single.just( - dummyUsers.filter { - !it.scheduledForDeletion - } - ) - - override fun getUserWithUsernameAndServer(username: String, server: String): Maybe = - Maybe.fromCallable { - dummyUsers.find { it.username == username } + override suspend fun getUsersNotScheduledForDeletion(): List = + dummyUsers.filter { + !it.scheduledForDeletion } - override fun setUserAsActiveWithId(id: Long): Int { + override suspend fun getUserWithUsernameAndServer(username: String, server: String): UserEntity? = + dummyUsers.find { it.username == username } + + override suspend fun setUserAsActiveWithId(id: Long): Int { activeUserId = id return 1 } - override fun updatePushState(id: Long, state: PushConfigurationState): Single { + override suspend fun updatePushState(id: Long, state: PushConfigurationState): Int { val index = dummyUsers.indexOfFirst { it.id == id } return if (index != -1) { dummyUsers[index] = dummyUsers[index] - Single.just(1) + 1 } else { - Single.just(0) + 0 } } } diff --git a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt index c7a01b1c8a5..7dd2c48a341 100644 --- a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt +++ b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/ConversationListFreshnessIntegrationTest.kt @@ -81,7 +81,9 @@ class ConversationListFreshnessIntegrationTest { db = Room.inMemoryDatabaseBuilder(context, TalkDatabase::class.java) .allowMainThreadQueries() .build() - db.usersDao().saveUser(UserEntity(id = ACCOUNT_ID, userId = "me", username = "me", baseUrl = BASE_URL)) + runBlocking { + db.usersDao().saveUser(UserEntity(id = ACCOUNT_ID, userId = "me", username = "me", baseUrl = BASE_URL)) + } whenever(networkMonitor.isOnline).thenReturn(MutableStateFlow(true)) diff --git a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/RoomListMessagePrefetchIntegrationTest.kt b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/RoomListMessagePrefetchIntegrationTest.kt index 20ec0e48905..9a6c862ae32 100644 --- a/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/RoomListMessagePrefetchIntegrationTest.kt +++ b/app/src/test/java/com/nextcloud/talk/conversationlist/data/network/RoomListMessagePrefetchIntegrationTest.kt @@ -71,7 +71,9 @@ class RoomListMessagePrefetchIntegrationTest { db = Room.inMemoryDatabaseBuilder(context, TalkDatabase::class.java) .allowMainThreadQueries() .build() - db.usersDao().saveUser(UserEntity(id = ACCOUNT_ID, userId = "me", username = "me", baseUrl = BASE_URL)) + runBlocking { + db.usersDao().saveUser(UserEntity(id = ACCOUNT_ID, userId = "me", username = "me", baseUrl = BASE_URL)) + } whenever(networkMonitor.isOnline).thenReturn(MutableStateFlow(true)) diff --git a/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt b/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt index b6bda9869c0..70bb649bef9 100644 --- a/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt +++ b/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt @@ -389,164 +389,173 @@ class LoginRepositoryTest { // ========== parseAndLogin() Tests ========== @Test - fun `parseAndLogin returns null when user is scheduled for deletion`() { - // Arrange - val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(true) - whenever(localLoginDataSource.startAccountRemovalWorker()) - .thenReturn(liveData) + fun `parseAndLogin returns null when user is scheduled for deletion`() = + runTest { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(true) + whenever(localLoginDataSource.startAccountRemovalWorker()) + .thenReturn(liveData) - // Act - val result = repo.parseAndLogin(loginData) + // Act + val result = repo.parseAndLogin(loginData) - // Assert - assertNull(result) - verify(localLoginDataSource).startAccountRemovalWorker() - } + // Assert + assertNull(result) + verify(localLoginDataSource).startAccountRemovalWorker() + } @Test - fun `parseAndLogin returns null when user exists and reAuth is false`() { - // Arrange - val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(false) - whenever(localLoginDataSource.checkIfUserExists(loginData)) - .thenReturn(true) + fun `parseAndLogin returns null when user exists and reAuth is false`() = + runTest { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(true) - // Act - val result = repo.parseAndLogin(loginData) + // Act + val result = repo.parseAndLogin(loginData) - // Assert - assertNull(result) - verify(localLoginDataSource, never()).updateUser(any()) - } + // Assert + assertNull(result) + verify(localLoginDataSource, never()).updateUser(any()) + } @Test - fun `parseAndLogin updates user when user exists and reAuth is true`() { - // Arrange - First set reAuth to true via QR flow - val qrData = "nc://login/server:https%3A//example.com" - repo.startLoginFlowFromQR(qrData, reAuth = true) + fun `parseAndLogin updates user when user exists and reAuth is true`() = + runTest { + // Arrange - First set reAuth to true via QR flow + val qrData = "nc://login/server:https%3A//example.com" + repo.startLoginFlowFromQR(qrData, reAuth = true) - val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(false) - whenever(localLoginDataSource.checkIfUserExists(loginData)) - .thenReturn(true) + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(true) - // Act - val result = repo.parseAndLogin(loginData) + // Act + val result = repo.parseAndLogin(loginData) - // Assert - assertNull(result) - verify(localLoginDataSource).updateUser(loginData) - } + // Assert + assertNull(result) + verify(localLoginDataSource).updateUser(loginData) + } @Test - fun `parseAndLogin returns Bundle for new user with https protocol`() { - // Arrange - val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(false) - whenever(localLoginDataSource.checkIfUserExists(loginData)) - .thenReturn(false) + fun `parseAndLogin returns Bundle for new user with https protocol`() = + runTest { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(false) - // Act - val result = repo.parseAndLogin(loginData) + // Act + val result = repo.parseAndLogin(loginData) - // Assert - assertNotNull(result) - assertTrue(result is Bundle) - } + // Assert + assertNotNull(result) + assertTrue(result is Bundle) + } @Test - fun `parseAndLogin returns Bundle for new user with http protocol`() { - // Arrange - val loginData = LoginCompletion(200, "http://server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(false) - whenever(localLoginDataSource.checkIfUserExists(loginData)) - .thenReturn(false) + fun `parseAndLogin returns Bundle for new user with http protocol`() = + runTest { + // Arrange + val loginData = LoginCompletion(200, "http://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(false) - // Act - val result = repo.parseAndLogin(loginData) + // Act + val result = repo.parseAndLogin(loginData) - // Assert - assertNotNull(result) - assertTrue(result is Bundle) - } + // Assert + assertNotNull(result) + assertTrue(result is Bundle) + } @Test - fun `parseAndLogin returns Bundle for new user without protocol prefix`() { - // Arrange - val loginData = LoginCompletion(200, "server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(false) - whenever(localLoginDataSource.checkIfUserExists(loginData)) - .thenReturn(false) + fun `parseAndLogin returns Bundle for new user without protocol prefix`() = + runTest { + // Arrange + val loginData = LoginCompletion(200, "server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(false) - // Act - val result = repo.parseAndLogin(loginData) + // Act + val result = repo.parseAndLogin(loginData) - // Assert - assertNotNull(result) - assertTrue(result is Bundle) - } + // Assert + assertNotNull(result) + assertTrue(result is Bundle) + } // ========== LocalLoginDataSource Integration Tests ========== @Test - fun `parseAndLogin properly integrates with LocalLoginDataSource checkIfUserIsScheduledForDeletion`() { - // Arrange - val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(true) - whenever(localLoginDataSource.startAccountRemovalWorker()) - .thenReturn(liveData) + fun `parseAndLogin properly integrates with LocalLoginDataSource checkIfUserIsScheduledForDeletion`() = + runTest { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(true) + whenever(localLoginDataSource.startAccountRemovalWorker()) + .thenReturn(liveData) - // Act - repo.parseAndLogin(loginData) + // Act + repo.parseAndLogin(loginData) - // Assert - verify(localLoginDataSource).checkIfUserIsScheduledForDeletion(loginData) - verify(localLoginDataSource).startAccountRemovalWorker() - } + // Assert + verify(localLoginDataSource).checkIfUserIsScheduledForDeletion(loginData) + verify(localLoginDataSource).startAccountRemovalWorker() + } @Test - fun `parseAndLogin properly integrates with LocalLoginDataSource checkIfUserExists`() { - // Arrange - val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(false) - whenever(localLoginDataSource.checkIfUserExists(loginData)) - .thenReturn(true) + fun `parseAndLogin properly integrates with LocalLoginDataSource checkIfUserExists`() = + runTest { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(true) - // Act - repo.parseAndLogin(loginData) + // Act + repo.parseAndLogin(loginData) - // Assert - verify(localLoginDataSource).checkIfUserExists(loginData) - verify(localLoginDataSource, never()).updateUser(any()) - } + // Assert + verify(localLoginDataSource).checkIfUserExists(loginData) + verify(localLoginDataSource, never()).updateUser(any()) + } @Test - fun `parseAndLogin calls updateUser with correct LoginCompletion data`() { - // Arrange - Set reAuth flag first - val qrData = "nc://login/server:https%3A//example.com" - repo.startLoginFlowFromQR(qrData, reAuth = true) + fun `parseAndLogin calls updateUser with correct LoginCompletion data`() = + runTest { + // Arrange - Set reAuth flag first + val qrData = "nc://login/server:https%3A//example.com" + repo.startLoginFlowFromQR(qrData, reAuth = true) - val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") - whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) - .thenReturn(false) - whenever(localLoginDataSource.checkIfUserExists(loginData)) - .thenReturn(true) + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(true) - // Act - repo.parseAndLogin(loginData) + // Act + repo.parseAndLogin(loginData) - // Assert - verify(localLoginDataSource).updateUser(loginData) - } + // Assert + verify(localLoginDataSource).updateUser(loginData) + } // ========== Edge Cases and Error Handling ========== diff --git a/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt b/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt index abc4f94b559..0df3573d41a 100644 --- a/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt +++ b/app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt @@ -8,17 +8,25 @@ package com.nextcloud.talk.users import com.nextcloud.talk.data.user.UsersRepository import com.nextcloud.talk.data.user.model.User -import io.reactivex.Maybe -import io.reactivex.Single +import com.nextcloud.talk.models.ExternalSignalingServer +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Before import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.check import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import org.mockito.kotlin.verifyBlocking +import org.mockito.kotlin.wheneverBlocking +@Suppress("DEPRECATION") class UserManagerTest { private val usersRepository: UsersRepository = mock() @@ -32,136 +40,433 @@ class UserManagerTest { // No row resolves as "the" active user unless a test overrides this, so // scheduleDuplicateAccountsForDeletion() falls back to the `current` flag / oldest row, // matching the behavior asserted by the tests below that don't care about this priority. - whenever(usersRepository.getActiveUser()).thenReturn(Maybe.empty()) + wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(null) + // activeUserSubject/activeUserStateFlow lazily collect this on init; an empty flow lets + // that background collection finish without racing the synchronous updates asserted below. + wheneverBlocking { usersRepository.getActiveUserFlow() }.thenReturn(emptyFlow()) } @Test - fun `keeps the current user among duplicates and schedules the rest for deletion`() { + fun `keeps the current user among duplicates and schedules the rest for deletion`() = + runTest { + val current = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) + val duplicate = user(id = 1, username = "userA", baseUrl = "https://example.com", current = false) + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(current, duplicate)) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(1, scheduledCount) + assertTrue(duplicate.scheduledForDeletion) + assertFalse(current.scheduledForDeletion) + verify(usersRepository).updateUser(duplicate) + } + + @Test + fun `keeps the oldest row when none of the duplicates is current`() = + runTest { + val oldest = user(id = 1, username = "userA", baseUrl = "https://example.com") + val newer = user(id = 2, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(newer, oldest)) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(1, scheduledCount) + assertTrue(newer.scheduledForDeletion) + assertFalse(oldest.scheduledForDeletion) + } + + @Test + fun `does nothing when there are no duplicates`() = + runTest { + val userA = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + val userB = user(id = 2, username = "userB", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(userA, userB)) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(0, scheduledCount) + assertFalse(userA.scheduledForDeletion) + assertFalse(userB.scheduledForDeletion) + } + + @Test + fun `different servers with the same username are not treated as duplicates`() = + runTest { + val userA = user(id = 1, username = "userA", baseUrl = "https://example.com") + val userB = user(id = 2, username = "userA", baseUrl = "https://other.example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(userA, userB)) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(0, scheduledCount) + } + + @Test + fun `rows with a null or blank username or baseUrl are never grouped as duplicates`() = + runTest { + val nullUsername = user(id = 1, username = "userA", baseUrl = "https://example.com") + .apply { username = null } + val anotherNullUsername = user(id = 2, username = "userA", baseUrl = "https://example.com") + .apply { username = null } + val blankBaseUrl = user(id = 3, username = "userA", baseUrl = "") + val anotherBlankBaseUrl = user(id = 4, username = "userA", baseUrl = "") + wheneverBlocking { usersRepository.getUsers() }.thenReturn( + listOf(nullUsername, anotherNullUsername, blankBaseUrl, anotherBlankBaseUrl) + ) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(0, scheduledCount) + } + + @Test + fun `keeps only one row out of three or more duplicates`() = + runTest { + val current = user(id = 3, username = "userA", baseUrl = "https://example.com", current = true) + val duplicate1 = user(id = 1, username = "userA", baseUrl = "https://example.com") + val duplicate2 = user(id = 2, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(duplicate1, duplicate2, current)) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(2, scheduledCount) + assertTrue(duplicate1.scheduledForDeletion) + assertTrue(duplicate2.scheduledForDeletion) + assertFalse(current.scheduledForDeletion) + } + + @Test + fun `handles multiple independent duplicate groups in one pass`() = + runTest { + val userACurrent = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + val userADuplicate = user(id = 2, username = "userA", baseUrl = "https://example.com") + val userBOldest = user(id = 3, username = "userB", baseUrl = "https://example.com") + val userBNewer = user(id = 4, username = "userB", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsers() }.thenReturn( + listOf(userACurrent, userADuplicate, userBNewer, userBOldest) + ) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(2, scheduledCount) + assertTrue(userADuplicate.scheduledForDeletion) + assertTrue(userBNewer.scheduledForDeletion) + assertFalse(userACurrent.scheduledForDeletion) + assertFalse(userBOldest.scheduledForDeletion) + } + + @Test + fun `does nothing when there are no users at all`() = + runTest { + wheneverBlocking { usersRepository.getUsers() }.thenReturn(emptyList()) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(0, scheduledCount) + } + + @Test + fun `keeps whichever row getActiveUser resolves to, even over a different row flagged current`() = + runTest { + // Simulates a past bug leaving two rows marked current=true for the same account: the + // active-user lookup (deterministically) resolves to one of them, but the other still + // carries the current flag too. The actively-resolved row must win, since it may be the + // one a live session/background sync is still bound to. + val staleCurrentFlag = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + val actuallyActive = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(staleCurrentFlag, actuallyActive)) + wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(actuallyActive) + + val scheduledCount = userManager.scheduleDuplicateAccountsForDeletionSuspend() + + assertEquals(1, scheduledCount) + assertTrue(staleCurrentFlag.scheduledForDeletion) + assertFalse(actuallyActive.scheduledForDeletion) + verify(usersRepository).updateUser(staleCurrentFlag) + } + + @Test + fun `old RxJava-typed bridge still delegates to the suspend implementation`() { val current = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) val duplicate = user(id = 1, username = "userA", baseUrl = "https://example.com", current = false) - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(current, duplicate))) + wheneverBlocking { usersRepository.getUsers() }.thenReturn(listOf(current, duplicate)) val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() assertEquals(1, scheduledCount) assertTrue(duplicate.scheduledForDeletion) - assertFalse(current.scheduledForDeletion) - verify(usersRepository).updateUser(duplicate) } @Test - fun `keeps the oldest row when none of the duplicates is current`() { - val oldest = user(id = 1, username = "userA", baseUrl = "https://example.com") - val newer = user(id = 2, username = "userA", baseUrl = "https://example.com") - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(newer, oldest))) + fun `currentUser returns the active user without touching any fallback`() { + val active = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(active) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val result = userManager.currentUser.blockingGet() - assertEquals(1, scheduledCount) - assertTrue(newer.scheduledForDeletion) - assertFalse(oldest.scheduledForDeletion) + assertEquals(active, result) + verifyBlocking(usersRepository, never()) { getUsersNotScheduledForDeletion() } } @Test - fun `does nothing when there are no duplicates`() { - val userA = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) - val userB = user(id = 2, username = "userB", baseUrl = "https://example.com") - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(userA, userB))) + fun `currentUser falls back to any non-deleted user and sets it active when none is active`() { + val fallback = user(id = 1, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(listOf(fallback)) + wheneverBlocking { usersRepository.setUserAsActiveWithId(fallback.id!!) }.thenReturn(true) + // getActiveUser() is re-queried after setUserAsActiveWithId() succeeds, simulating the DB + // now reporting the freshly-activated row. + wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(null, fallback) + + val result = userManager.currentUser.blockingGet() + + assertEquals(fallback, result) + verifyBlocking(usersRepository) { setUserAsActiveWithId(fallback.id!!) } + } - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + @Test + fun `currentUser is empty when there is no active user and none to fall back to`() { + wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(emptyList()) - assertEquals(0, scheduledCount) - assertFalse(userA.scheduledForDeletion) - assertFalse(userB.scheduledForDeletion) + assertTrue(userManager.currentUser.isEmpty.blockingGet()) } @Test - fun `different servers with the same username are not treated as duplicates`() { - val userA = user(id = 1, username = "userA", baseUrl = "https://example.com") - val userB = user(id = 2, username = "userA", baseUrl = "https://other.example.com") - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(userA, userB))) + fun `deleteUserSuspend does nothing and returns 0 when the user does not exist`() = + runTest { + wheneverBlocking { usersRepository.getUserWithId(42L) }.thenReturn(null) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val result = userManager.deleteUserSuspend(42L) - assertEquals(0, scheduledCount) - } + assertEquals(0, result) + verify(usersRepository, never()).deleteUser(any()) + } @Test - fun `rows with a null or blank username or baseUrl are never grouped as duplicates`() { - val nullUsername = user(id = 1, username = "userA", baseUrl = "https://example.com") - .apply { username = null } - val anotherNullUsername = user(id = 2, username = "userA", baseUrl = "https://example.com") - .apply { username = null } - val blankBaseUrl = user(id = 3, username = "userA", baseUrl = "") - val anotherBlankBaseUrl = user(id = 4, username = "userA", baseUrl = "") - whenever(usersRepository.getUsers()).thenReturn( - Single.just(listOf(nullUsername, anotherNullUsername, blankBaseUrl, anotherBlankBaseUrl)) - ) + fun `deleteUserSuspend deletes the user when it exists`() = + runTest { + val existing = user(id = 42, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUserWithId(42L) }.thenReturn(existing) + wheneverBlocking { usersRepository.deleteUser(existing) }.thenReturn(1) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val result = userManager.deleteUserSuspend(42L) - assertEquals(0, scheduledCount) - } + assertEquals(1, result) + verify(usersRepository).deleteUser(existing) + } @Test - fun `keeps only one row out of three or more duplicates`() { - val current = user(id = 3, username = "userA", baseUrl = "https://example.com", current = true) - val duplicate1 = user(id = 1, username = "userA", baseUrl = "https://example.com") - val duplicate2 = user(id = 2, username = "userA", baseUrl = "https://example.com") - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(duplicate1, duplicate2, current))) + fun `checkIfUserIsScheduledForDeletionSuspend reflects the matching user's flag`() = + runTest { + val scheduled = user(id = 1, username = "userA", baseUrl = "https://example.com") + .apply { scheduledForDeletion = true } + wheneverBlocking { + usersRepository.getUserWithUsernameAndServer("userA", "https://example.com") + }.thenReturn(scheduled) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + assertTrue(userManager.checkIfUserIsScheduledForDeletionSuspend("userA", "https://example.com")) + } - assertEquals(2, scheduledCount) - assertTrue(duplicate1.scheduledForDeletion) - assertTrue(duplicate2.scheduledForDeletion) - assertFalse(current.scheduledForDeletion) - } + @Test + fun `checkIfUserIsScheduledForDeletionSuspend is false when the user does not exist`() = + runTest { + wheneverBlocking { + usersRepository.getUserWithUsernameAndServer("userA", "https://example.com") + }.thenReturn(null) + + assertFalse(userManager.checkIfUserIsScheduledForDeletionSuspend("userA", "https://example.com")) + } @Test - fun `handles multiple independent duplicate groups in one pass`() { - val userACurrent = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) - val userADuplicate = user(id = 2, username = "userA", baseUrl = "https://example.com") - val userBOldest = user(id = 3, username = "userB", baseUrl = "https://example.com") - val userBNewer = user(id = 4, username = "userB", baseUrl = "https://example.com") - whenever(usersRepository.getUsers()).thenReturn( - Single.just(listOf(userACurrent, userADuplicate, userBNewer, userBOldest)) - ) + fun `checkIfUserExistsSuspend is true only when a matching user is found`() = + runTest { + wheneverBlocking { + usersRepository.getUserWithUsernameAndServer("userA", "https://example.com") + }.thenReturn(user(id = 1, username = "userA", baseUrl = "https://example.com")) + wheneverBlocking { + usersRepository.getUserWithUsernameAndServer("userB", "https://example.com") + }.thenReturn(null) + + assertTrue(userManager.checkIfUserExistsSuspend("userA", "https://example.com")) + assertFalse(userManager.checkIfUserExistsSuspend("userB", "https://example.com")) + } - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + @Test + fun `scheduleUserForDeletionWithIdSuspend returns false when the user does not exist`() = + runTest { + wheneverBlocking { usersRepository.getUserWithId(99L) }.thenReturn(null) - assertEquals(2, scheduledCount) - assertTrue(userADuplicate.scheduledForDeletion) - assertTrue(userBNewer.scheduledForDeletion) - assertFalse(userACurrent.scheduledForDeletion) - assertFalse(userBOldest.scheduledForDeletion) - } + assertFalse(userManager.scheduleUserForDeletionWithIdSuspend(99L)) + verify(usersRepository, never()).updateUser(any()) + } @Test - fun `does nothing when there are no users at all`() { - whenever(usersRepository.getUsers()).thenReturn(Single.just(emptyList())) + fun `scheduleUserForDeletionWithIdSuspend marks the user deleted and returns false with nobody left to activate`() = + runTest { + val target = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + wheneverBlocking { usersRepository.getUserWithId(1L) }.thenReturn(target) + wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(emptyList()) - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + val result = userManager.scheduleUserForDeletionWithIdSuspend(1L) - assertEquals(0, scheduledCount) - } + assertFalse(result) + assertTrue(target.scheduledForDeletion) + assertFalse(target.current) + verify(usersRepository).updateUser(target) + } @Test - fun `keeps whichever row getActiveUser resolves to, even over a different row flagged current`() { - // Simulates a past bug leaving two rows marked current=true for the same account: the - // active-user lookup (deterministically) resolves to one of them, but the other still - // carries the current flag too. The actively-resolved row must win, since it may be the - // one a live session/background sync is still bound to. - val staleCurrentFlag = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) - val actuallyActive = user(id = 2, username = "userA", baseUrl = "https://example.com", current = true) - whenever(usersRepository.getUsers()).thenReturn(Single.just(listOf(staleCurrentFlag, actuallyActive))) - whenever(usersRepository.getActiveUser()).thenReturn(Maybe.just(actuallyActive)) + fun `scheduleUserForDeletionWithIdSuspend returns true and activates another user when one remains`() = + runTest { + val target = user(id = 1, username = "userA", baseUrl = "https://example.com", current = true) + val other = user(id = 2, username = "userB", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.getUserWithId(1L) }.thenReturn(target) + wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(listOf(other)) + wheneverBlocking { usersRepository.setUserAsActiveWithId(other.id!!) }.thenReturn(true) + wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(other) + + val result = userManager.scheduleUserForDeletionWithIdSuspend(1L) + + assertTrue(result) + assertTrue(target.scheduledForDeletion) + verify(usersRepository).setUserAsActiveWithId(other.id!!) + } - val scheduledCount = userManager.scheduleDuplicateAccountsForDeletion().blockingGet() + @Test + fun `updateExternalSignalingServerSuspend throws when the user does not exist`() = + runTest { + wheneverBlocking { usersRepository.getUserWithId(7L) }.thenReturn(null) + + try { + userManager.updateExternalSignalingServerSuspend(7L, ExternalSignalingServer()) + fail("Expected NoSuchElementException") + } catch (expected: NoSuchElementException) { + // expected + } + } - assertEquals(1, scheduledCount) - assertTrue(staleCurrentFlag.scheduledForDeletion) - assertFalse(actuallyActive.scheduledForDeletion) - verify(usersRepository).updateUser(staleCurrentFlag) - } + @Test + fun `updateExternalSignalingServerSuspend updates the matching user`() = + runTest { + val existing = user(id = 7, username = "userA", baseUrl = "https://example.com") + val server = ExternalSignalingServer(externalSignalingServer = "https://signaling.example.com") + wheneverBlocking { usersRepository.getUserWithId(7L) }.thenReturn(existing) + wheneverBlocking { usersRepository.updateUser(existing) }.thenReturn(1) + + val result = userManager.updateExternalSignalingServerSuspend(7L, server) + + assertEquals(1, result) + assertEquals(server, existing.externalSignalingServer) + } + + @Test + fun `updateOrCreateUserSuspend inserts a user without an id`() = + runTest { + val newUser = User(id = null, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.insertUser(newUser) }.thenReturn(5L) + + val result = userManager.updateOrCreateUserSuspend(newUser) + + assertEquals(5, result) + verify(usersRepository, never()).updateUser(any()) + } + + @Test + fun `updateOrCreateUserSuspend updates a user that already has an id`() = + runTest { + val existing = user(id = 3, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.updateUser(existing) }.thenReturn(1) + + val result = userManager.updateOrCreateUserSuspend(existing) + + assertEquals(1, result) + verify(usersRepository, never()).insertUser(any()) + } + + @Test + fun `setUserAsActiveSuspend publishes the new user on currentUserFlow only when it succeeds`() = + runTest { + val target = user(id = 1, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.setUserAsActiveWithId(1L) }.thenReturn(true) + + val result = userManager.setUserAsActiveSuspend(target) + + assertTrue(result) + assertEquals(target, userManager.currentUserFlow.value) + } + + @Test + fun `setUserAsActiveSuspend leaves currentUserFlow untouched when it fails`() = + runTest { + val target = user(id = 1, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.setUserAsActiveWithId(1L) }.thenReturn(false) + + val result = userManager.setUserAsActiveSuspend(target) + + assertFalse(result) + assertNull(userManager.currentUserFlow.value) + } + + @Test + fun `storeProfileSuspend creates a new user when the attributes carry no id`() = + runTest { + val attributes = UserManager.UserAttributes( + id = null, + serverUrl = "https://example.com", + currentUser = true, + userId = "userId", + token = "token", + displayName = "Display Name", + pushConfigurationState = null, + // createUser() guards these with TextUtils.isEmpty(), which this project's unit + // tests stub to always return false (testOptions.unitTests.isReturnDefaultValues), + // so a null value here would still hit LoganSquare.parse(null, ...) and NPE. + capabilities = "{}", + serverVersion = "{}", + certificateAlias = null, + externalSignalingServer = "{}" + ) + val stored = user(id = 10, username = "userA", baseUrl = "https://example.com") + wheneverBlocking { usersRepository.insertUser(any()) }.thenReturn(10L) + wheneverBlocking { usersRepository.getUserWithId(10L) }.thenReturn(stored) + + val result = userManager.storeProfileSuspend("userA", attributes) + + assertEquals(stored, result) + verify(usersRepository).insertUser( + check { + assertEquals("userA", it.username) + assertEquals("https://example.com", it.baseUrl) + assertEquals("token", it.token) + assertEquals("Display Name", it.displayName) + } + ) + } + + @Test + fun `storeProfileSuspend updates the existing user resolved from the attributes' id`() = + runTest { + val existing = user(id = 10, username = "userA", baseUrl = "https://old.example.com") + val attributes = UserManager.UserAttributes( + id = 10, + serverUrl = "https://new.example.com", + currentUser = true, + userId = "userId", + token = "newToken", + displayName = "New Display Name", + pushConfigurationState = null, + capabilities = null, + serverVersion = null, + certificateAlias = null, + externalSignalingServer = null + ) + wheneverBlocking { usersRepository.getUserWithId(10L) }.thenReturn(existing) + wheneverBlocking { usersRepository.insertUser(existing) }.thenReturn(10L) + + val result = userManager.storeProfileSuspend("userA", attributes) + + assertEquals("https://new.example.com", existing.baseUrl) + assertEquals("newToken", existing.token) + assertEquals("New Display Name", existing.displayName) + assertEquals(existing, result) + } }