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 a26b37fa4b0..92d72e2668a 100644 --- a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt @@ -240,7 +240,7 @@ class AccountVerificationActivity : BaseActivity() { UserManager.UserAttributes( id = null, serverUrl = baseUrl, - currentUser = true, + currentUser = false, userId = userId, token = token, displayName = displayName, @@ -519,32 +519,22 @@ class AccountVerificationActivity : BaseActivity() { private fun proceedWithLogin() { cookieManager.cookieStore.removeAll() - if (userManager.users.blockingGet().size == 1 || - currentUserProviderOld.currentUser.blockingGet().id != internalAccountId - ) { - val userToSetAsActive = userManager.getUserWithId(internalAccountId).blockingGet() - Log.d(TAG, "userToSetAsActive: " + userToSetAsActive.username) + val userToSetAsActive = userManager.getUserWithId(internalAccountId).blockingGet() + Log.d(TAG, "userToSetAsActive: " + userToSetAsActive.username) - if (userManager.setUserAsActive(userToSetAsActive).blockingGet()) { - runOnUiThread { - if (userManager.users.blockingGet().size == 1) { - val intent = Intent(context, ConversationsListActivity::class.java) - startActivity(intent) - } else { - if (isAccountImport) { - ApplicationWideMessageHolder.getInstance().messageType = - ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED - } - val intent = Intent(context, ConversationsListActivity::class.java) - startActivity(intent) - } + if (userManager.setUserAsActive(userToSetAsActive).blockingGet()) { + runOnUiThread { + if (userManager.users.blockingGet().size > 1 && isAccountImport) { + ApplicationWideMessageHolder.getInstance().messageType = + ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED } - } else { - Log.e(TAG, "failed to set active user") - Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + val intent = Intent(context, ConversationsListActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) } } else { - Log.d(TAG, "continuing proceedWithLogin was skipped for this user") + Log.e(TAG, "failed to set active user") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() } } 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 7ec21adbbf6..8a785130a1b 100644 --- a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt +++ b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt @@ -19,6 +19,10 @@ import com.nextcloud.talk.models.json.push.PushConfigurationState import io.reactivex.Maybe import io.reactivex.Observable import io.reactivex.Single +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow @Suppress("TooManyFunctions") class UserManager internal constructor(private val userRepository: UsersRepository) { @@ -34,10 +38,41 @@ class UserManager internal constructor(private val userRepository: UsersReposito .switchIfEmpty(Maybe.defer { getAnyUserAndSetAsActive() }) } + /** + * Backed by [activeUserSubject] rather than [UsersRepository.getActiveUserObservable] 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]. + * + * 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 + * bridging on the consuming side. + */ val currentUserObservable: Observable - get() { - return userRepository.getActiveUserObservable() - } + get() = activeUserSubject + + /** + * Coroutine-native counterpart to [currentUserObservable] - see its doc for why this exists. Both are + * updated synchronously, at the same point in [setUserAsActive], from Room's same underlying query. + */ + val currentUserFlow: StateFlow + get() = activeUserStateFlow + + private val activeUserSubject: Subject by lazy { + val subject = BehaviorSubject.create().toSerialized() + userRepository.getActiveUserObservable().subscribe(subject::onNext) { } + subject + } + + private val activeUserStateFlow: MutableStateFlow by lazy { + val flow = MutableStateFlow(null) + userRepository.getActiveUserObservable().subscribe({ flow.value = it }) { } + flow + } fun deleteUser(internalId: Long): Int = userRepository.deleteUser(userRepository.getUserWithId(internalId).blockingGet()) @@ -156,6 +191,12 @@ class UserManager internal constructor(private val userRepository: UsersReposito fun setUserAsActive(user: User): Single { Log.d(TAG, "setUserAsActive:" + user.id!!) return userRepository.setUserAsActiveWithId(user.id!!) + .doOnSuccess { success -> + if (success) { + activeUserSubject.onNext(user) + activeUserStateFlow.value = user + } + } } fun storeProfile(username: String?, userAttributes: UserAttributes): Maybe = diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt index 25bfdbe0f6c..abc2ee78204 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt @@ -9,16 +9,9 @@ package com.nextcloud.talk.utils.database.user import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.users.UserManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.rx2.asFlow import kotlinx.coroutines.withTimeoutOrNull import javax.inject.Inject import javax.inject.Singleton @@ -26,18 +19,8 @@ import javax.inject.Singleton @Singleton class CurrentUserProviderImpl @Inject constructor(private val userManager: UserManager) : CurrentUserProvider { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - private val currentUser: StateFlow = userManager.currentUserObservable - .asFlow() - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = null - ) - // only emit non-null users - override val currentUserFlow: Flow = currentUser.filterNotNull() + override val currentUserFlow: Flow = userManager.currentUserFlow.filterNotNull() // function for safe one-shot access override suspend fun getCurrentUser(timeout: Long): Result { diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt index 4d42d4e041b..07036cc8583 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt @@ -14,6 +14,7 @@ import com.nextcloud.talk.users.UserManager import dagger.Binds import dagger.Module import dagger.Provides +import javax.inject.Singleton @Module(includes = [DatabaseModule::class]) abstract class UserModule { @@ -28,6 +29,7 @@ abstract class UserModule { companion object { @Provides + @Singleton fun provideUserManager(userRepository: UsersRepository): UserManager = UserManager(userRepository) } }