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 @@ -40,15 +40,15 @@ import kotlinx.coroutines.sync.withPermit

class AppAccountFeatureRepositories(
private val account: StateFlow<Account?>,
private val saveAccount: suspend (Account) -> Unit,
private val saveAccount: suspend (Account?, Account) -> Unit,
private val clearAccount: suspend () -> Unit,
private val database: TiebaPureDatabase,
private val repositories: TiebaRepositories,
private val authenticationRepository: AuthenticationRepository,
private val accountRepository: AccountRepository,
private val mutationRepository: AccountMutationRepository,
) {
private val favoritePostIds = ConcurrentHashMap<Long, ULong>()
private val favoritePostIds = ConcurrentHashMap<Pair<String, Long>, ULong>()
private val historyEntries: Flow<List<BrowsingHistoryEntry>> = combine(
database.browsingHistoryDao().observeAll(),
database.blocklistDao().observeAll(),
Expand Down Expand Up @@ -79,7 +79,7 @@ class AppAccountFeatureRepositories(
}
authenticationRepository.validateLogin(credentials).also {
ensureUnchangedSession(initialAccount)
saveAccount(it)
saveAccount(initialAccount, it)
}
}

Expand Down Expand Up @@ -175,10 +175,10 @@ class AppAccountFeatureRepositories(
result.copy(favorites = result.favorites.filter {
TiebaContentFilterPolicy.shouldKeep(it, blocklist)
})
}
}.also { result ->
result.favorites.forEach { favorite ->
favoritePostIds[favorite.threadId] = favorite.markedPostId ?: 0u
}.also { result ->
result.favorites.forEach { favorite ->
favoritePostIds[current.id to favorite.threadId] = favorite.markedPostId ?: 0u
}
}
}

Expand All @@ -197,11 +197,11 @@ class AppAccountFeatureRepositories(
mutationRepository.setThreadFavorite(
account = current,
threadId = threadId,
postId = favoritePostIds[threadId] ?: 0u,
postId = favoritePostIds[current.id to threadId] ?: 0u,
favorited = false,
)
ensureCurrentSession(current)
favoritePostIds.remove(threadId)
favoritePostIds.remove(current.id to threadId)
removed += threadId
} catch (error: CancellationException) {
throw error
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package dev.infinityf4p.tiebapure

import dev.infinityf4p.tiebapure.core.data.AccountCredentialState
import dev.infinityf4p.tiebapure.core.model.Account
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext

internal class AppAccountSessionController(
initialState: AccountCredentialState,
private val saveState: (AccountCredentialState) -> Unit,
private val clearState: () -> Unit,
private val activateSession: suspend (Account) -> Unit,
private val invalidateAndDrain: suspend (Account) -> Unit,
) {
private val mutex = Mutex()
private var state = initialState
private val mutableAccount = MutableStateFlow(initialState.activeAccount)
private val mutableAccounts = MutableStateFlow(initialState.accounts)

val account: StateFlow<Account?> = mutableAccount.asStateFlow()
val accounts: StateFlow<List<Account>> = mutableAccounts.asStateFlow()

suspend fun addOrReplace(account: Account, expectedCurrent: Account?): Boolean = mutex.withLock {
check(state.activeAccount?.sessionIdentity() == expectedCurrent?.sessionIdentity()) {
"登录状态已变化,请重试。"
}
transitionTo(state.addOrReplace(account))
}

suspend fun switchTo(accountId: String): Boolean = mutex.withLock {
transitionTo(state.switchTo(accountId))
}

suspend fun remove(accountId: String): Boolean = mutex.withLock {
transitionTo(state.remove(accountId))
}

suspend fun removeCurrent(expected: Account): Boolean = mutex.withLock {
if (state.activeAccount?.sessionIdentity() != expected.sessionIdentity()) return@withLock false
transitionTo(state.remove(expected.id))
true
}

private suspend fun transitionTo(nextState: AccountCredentialState): Boolean {
if (nextState == state) return false
val previousAccount = state.activeAccount
val nextAccount = nextState.activeAccount
val sessionChanged = previousAccount?.sessionIdentity() != nextAccount?.sessionIdentity()
if (!sessionChanged) {
persist(nextState)
publish(nextState)
return false
}

var previousInvalidated = false
var nextActivated = false
try {
previousAccount?.let {
invalidateAndDrain(it)
previousInvalidated = true
}
nextAccount?.let {
activateSession(it)
nextActivated = true
}
persist(nextState)
publish(nextState)
return true
} catch (error: Throwable) {
withContext(NonCancellable) {
if (nextActivated && nextAccount != null) {
runCatching { invalidateAndDrain(nextAccount) }
.exceptionOrNull()
?.let(error::addSuppressed)
}
if (previousInvalidated && previousAccount != null) {
runCatching { activateSession(previousAccount) }
.exceptionOrNull()
?.let(error::addSuppressed)
}
}
throw error
}
}

private fun persist(nextState: AccountCredentialState) {
if (nextState.accounts.isEmpty()) clearState() else saveState(nextState)
}

private fun publish(nextState: AccountCredentialState) {
state = nextState
mutableAccounts.value = nextState.accounts
mutableAccount.value = nextState.activeAccount
}
}
79 changes: 41 additions & 38 deletions app/src/main/kotlin/dev/infinityf4p/tiebapure/AppContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
Expand All @@ -37,14 +35,6 @@ import kotlinx.coroutines.launch
class AppContainer(context: Context) {
private val appContext = context.applicationContext
private val credentialVault = AccountCredentialVault(appContext)
private val mutableAccount = MutableStateFlow(credentialVault.load())

val sessionExpiration = SessionExpirationCoordinator(
currentAccount = { mutableAccount.value },
logOut = ::logOut,
)

val account: StateFlow<Account?> = mutableAccount.asStateFlow()
val settings = AppSettingsStore(appContext)
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val currentSettings = settings.values.stateIn(
Expand All @@ -63,6 +53,19 @@ class AppContainer(context: Context) {
)
private val accountService = DefaultTiebaAccountService(transport, requestBuilder)
private val writeService = DefaultTiebaWriteService(transport, accountService, requestBuilder)
private val accountSessions = AppAccountSessionController(
initialState = credentialVault.load(),
saveState = credentialVault::save,
clearState = credentialVault::clear,
activateSession = writeService::activateSession,
invalidateAndDrain = writeService::invalidateAndDrain,
)
val account: StateFlow<Account?> = accountSessions.account
val accounts: StateFlow<List<Account>> = accountSessions.accounts
val sessionExpiration = SessionExpirationCoordinator(
currentAccount = { account.value },
logOut = ::removeCurrentAccount,
)
val authenticationRepository = NetworkAuthenticationRepository(accountService)
val accountRepository = NetworkAccountRepository(accountService)
.monitorSessions(sessionExpiration::report)
Expand All @@ -74,14 +77,14 @@ class AppContainer(context: Context) {
val savedThreads = AppSavedThreadRepository(
database = database,
repositories = repositories,
account = { mutableAccount.value },
account = { account.value },
mediaStore = savedThreadMedia,
context = appContext,
)
val featureRepositories = AppFeatureRepositories(
repositories = repositories,
database = database,
account = { mutableAccount.value },
account = { account.value },
accountRepository = accountRepository,
mutationRepository = mutationRepository,
settings = { currentSettings.value },
Expand All @@ -102,7 +105,7 @@ class AppContainer(context: Context) {
account = account,
accountRepository = accountRepository,
mutationRepository = mutationRepository,
onLogout = ::clearStoredAccount,
onLogout = ::removeCurrentAccount,
automaticSignStore = AutomaticSignStore(appContext),
)
val composerRepository = AppComposerRepository(
Expand Down Expand Up @@ -146,37 +149,37 @@ class AppContainer(context: Context) {
profilePendingEdit = profile
}

suspend fun replaceAccount(value: Account) {
val previous = mutableAccount.value
if (previous?.sessionIdentity() != value.sessionIdentity()) {
previous?.let { mutationRepository.invalidateAndDrain(it) }
}
mutationRepository.activateSession(value)
try {
credentialVault.save(value)
mutableAccount.value = value
} catch (error: Throwable) {
mutationRepository.invalidateAndDrain(value)
previous?.let { mutationRepository.activateSession(it) }
throw error
}
private suspend fun replaceAccount(expectedCurrent: Account?, value: Account) {
if (accountSessions.addOrReplace(value, expectedCurrent)) resetSessionState()
}

suspend fun logOut() {
val current = mutableAccount.value
try {
current?.let { mutationRepository.invalidateAndDrain(it) }
} finally {
if (current == null || mutableAccount.value?.sessionIdentity() == current.sessionIdentity()) {
clearStoredAccount()
}
account.value?.let { removeCurrentAccount(it) }
}

suspend fun switchAccount(accountId: String) {
if (accountSessions.switchTo(accountId)) resetSessionState()
}

suspend fun removeAccount(accountId: String) {
val sessionChanged = accountSessions.remove(accountId)
if (sessionChanged) {
resetSessionState()
clearBaiduWebSession()
}
}

private suspend fun removeCurrentAccount(expected: Account) {
if (accountSessions.removeCurrent(expected)) {
resetSessionState()
clearBaiduWebSession()
}
}

private fun clearStoredAccount() {
credentialVault.clear()
mutableAccount.value = null
clearBaiduWebSession()
private fun resetSessionState() {
profilePendingEdit = null
featureRepositories.clearSessionCaches()
sessionExpiration.dismissNotice()
}

private fun deviceProfile(context: Context): TiebaDeviceProfile {
Expand Down
Loading