Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class AccountVerificationActivity : BaseActivity() {
UserManager.UserAttributes(
id = null,
serverUrl = baseUrl,
currentUser = true,
currentUser = false,
userId = userId,
token = token,
displayName = displayName,
Expand Down Expand Up @@ -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()
}
}

Expand Down
47 changes: 44 additions & 3 deletions app/src/main/java/com/nextcloud/talk/users/UserManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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<User>
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<User?>
get() = activeUserStateFlow

private val activeUserSubject: Subject<User> by lazy {
val subject = BehaviorSubject.create<User>().toSerialized()
userRepository.getActiveUserObservable().subscribe(subject::onNext) { }
subject
}

private val activeUserStateFlow: MutableStateFlow<User?> by lazy {
val flow = MutableStateFlow<User?>(null)
userRepository.getActiveUserObservable().subscribe({ flow.value = it }) { }
flow
}

fun deleteUser(internalId: Long): Int =
userRepository.deleteUser(userRepository.getUserWithId(internalId).blockingGet())
Expand Down Expand Up @@ -156,6 +191,12 @@ class UserManager internal constructor(private val userRepository: UsersReposito
fun setUserAsActive(user: User): Single<Boolean> {
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<User> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,18 @@ 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

@Singleton
class CurrentUserProviderImpl @Inject constructor(private val userManager: UserManager) : CurrentUserProvider {

private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

private val currentUser: StateFlow<User?> = userManager.currentUserObservable
.asFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = null
)

// only emit non-null users
override val currentUserFlow: Flow<User> = currentUser.filterNotNull()
override val currentUserFlow: Flow<User> = userManager.currentUserFlow.filterNotNull()

// function for safe one-shot access
override suspend fun getCurrentUser(timeout: Long): Result<User> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,6 +29,7 @@ abstract class UserModule {

companion object {
@Provides
@Singleton
fun provideUserManager(userRepository: UsersRepository): UserManager = UserManager(userRepository)
}
}
Loading