remove SyncableModel and SyncUtils

not needed for our requirements + it simplifies code

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2024-08-09 14:11:06 +02:00
parent a84e69b6bb
commit fe4bf942ab
No known key found for this signature in database
GPG key ID: C793F8B59F43CE7B
9 changed files with 46 additions and 175 deletions

View file

@ -1,6 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@ -161,7 +162,7 @@ class OfflineFirstChatRepository @Inject constructor(
val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId) val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId)
if (loadFromServer && monitor.isOnline.first()) { if (loadFromServer) {
sync(withNetworkParams) sync(withNetworkParams)
} }
@ -292,7 +293,6 @@ class OfflineFirstChatRepository @Inject constructor(
val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId) val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId)
if (loadFromServer) { if (loadFromServer) {
val fieldMap = getFieldMap( val fieldMap = getFieldMap(
lookIntoFuture = false, lookIntoFuture = false,
includeLastKnown = true, includeLastKnown = true,
@ -368,6 +368,11 @@ class OfflineFirstChatRepository @Inject constructor(
} }
private suspend fun sync(bundle: Bundle): List<ChatMessageEntity>? { private suspend fun sync(bundle: Bundle): List<ChatMessageEntity>? {
if (!monitor.isOnline.first()) {
Log.d(TAG, "Device is offline, can't load chat messages from server")
return null
}
val result = getMessagesFromServer(bundle) ?: return listOf() val result = getMessagesFromServer(bundle) ?: return listOf()
var chatMessagesFromSync: List<ChatMessageEntity>? = null var chatMessagesFromSync: List<ChatMessageEntity>? = null

View file

@ -7,12 +7,11 @@
package com.nextcloud.talk.conversationlist.data package com.nextcloud.talk.conversationlist.data
import com.nextcloud.talk.data.sync.Syncable
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface OfflineConversationsRepository : Syncable { interface OfflineConversationsRepository {
/** /**
* Stream of a list of rooms, for use in the conversation list. * Stream of a list of rooms, for use in the conversation list.

View file

@ -1,24 +1,23 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com> * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
package com.nextcloud.talk.conversationlist.data.network package com.nextcloud.talk.conversationlist.data.network
import android.os.Bundle import android.util.Log
import androidx.core.os.bundleOf import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
import com.nextcloud.talk.data.database.dao.ConversationsDao import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.database.mappers.asEntity import com.nextcloud.talk.data.database.mappers.asEntity
import com.nextcloud.talk.data.database.mappers.asModel import com.nextcloud.talk.data.database.mappers.asModel
import com.nextcloud.talk.data.database.model.ConversationEntity import com.nextcloud.talk.data.database.model.ConversationEntity
import com.nextcloud.talk.data.sync.Synchronizer import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.sync.changeListSync
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
@ -35,9 +34,9 @@ import javax.inject.Inject
class OfflineFirstConversationsRepository @Inject constructor( class OfflineFirstConversationsRepository @Inject constructor(
private val dao: ConversationsDao, private val dao: ConversationsDao,
private val network: ConversationsNetworkDataSource, private val network: ConversationsNetworkDataSource,
private val monitor: NetworkMonitor,
private val currentUserProviderNew: CurrentUserProviderNew private val currentUserProviderNew: CurrentUserProviderNew
) : OfflineConversationsRepository, Synchronizer { ) : OfflineConversationsRepository {
override val roomListFlow: Flow<List<ConversationModel>> override val roomListFlow: Flow<List<ConversationModel>>
get() = _roomListFlow get() = _roomListFlow
private val _roomListFlow: MutableSharedFlow<List<ConversationModel>> = MutableSharedFlow() private val _roomListFlow: MutableSharedFlow<List<ConversationModel>> = MutableSharedFlow()
@ -56,7 +55,7 @@ class OfflineFirstConversationsRepository @Inject constructor(
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
_roomListFlow.emit(list) _roomListFlow.emit(list)
} }
this@OfflineFirstConversationsRepository.sync(bundleOf()) sync()
} }
} }
@ -64,39 +63,28 @@ class OfflineFirstConversationsRepository @Inject constructor(
scope.launch { scope.launch {
val id = user.id!! val id = user.id!!
val model = getConversation(id, roomToken) val model = getConversation(id, roomToken)
model?.let { _conversationFlow.emit(model) } model.let { _conversationFlow.emit(model) }
} }
override suspend fun syncWith(bundle: Bundle, synchronizer: Synchronizer): Boolean = private suspend fun sync() {
synchronizer.changeListSync( if (!monitor.isOnline.first()) {
modelFetcher = { Log.d(OfflineFirstChatRepository.TAG, "Device is offline, can't load conversations from server")
return@changeListSync getConversationsFromServer() return
}, }
// not needed
versionUpdater = {}, try {
modelDeleter = {}, val conversationsList = network.getRooms(user, user.baseUrl!!, false)
modelUpdater = { models -> .subscribeOn(Schedulers.io())
val list = models.filterIsInstance<Conversation>().map { .observeOn(AndroidSchedulers.mainThread())
.blockingSingle()
val list = conversationsList.map {
it.asEntity(user.id!!) it.asEntity(user.id!!)
} }
dao.upsertConversations(list) dao.upsertConversations(list)
} catch (e: Exception) {
Log.e(TAG, "Something went wrong when fetching conversations", e)
} }
)
private fun getConversationsFromServer(): List<Conversation> {
val list = network.getRooms(user, user.baseUrl!!, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map { list ->
return@map list.map {
it.apply {
id = roomId!!.toLong()
}
}
}
.blockingSingle()
return list ?: listOf()
} }
private suspend fun getListOfConversations(accountId: Long): List<ConversationModel> = private suspend fun getListOfConversations(accountId: Long): List<ConversationModel> =
@ -104,8 +92,12 @@ class OfflineFirstConversationsRepository @Inject constructor(
it.map(ConversationEntity::asModel) it.map(ConversationEntity::asModel)
}.first() }.first()
private suspend fun getConversation(accountId: Long, token: String): ConversationModel? { private suspend fun getConversation(accountId: Long, token: String): ConversationModel {
val entity = dao.getConversationForUser(accountId, token).first() val entity = dao.getConversationForUser(accountId, token).first()
return entity?.asModel() return entity.asModel()
}
companion object {
val TAG = OfflineFirstConversationsRepository::class.simpleName
} }
} }

View file

@ -191,9 +191,10 @@ class RepositoryModule {
fun provideOfflineFirstConversationsRepository( fun provideOfflineFirstConversationsRepository(
dao: ConversationsDao, dao: ConversationsDao,
dataSource: ConversationsNetworkDataSource, dataSource: ConversationsNetworkDataSource,
networkMonitor: NetworkMonitor,
currentUserProviderNew: CurrentUserProviderNew currentUserProviderNew: CurrentUserProviderNew
): OfflineConversationsRepository { ): OfflineConversationsRepository {
return OfflineFirstConversationsRepository(dao, dataSource, currentUserProviderNew) return OfflineFirstConversationsRepository(dao, dataSource, networkMonitor, currentUserProviderNew)
} }
@Provides @Provides

View file

@ -1,25 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.data.changeListVersion
/**
* Models any changes from the network, agnostic to what data is being modeled.
* Implemented by Models that support offline synchronization.
*/
interface SyncableModel {
/**
* Model identifier.
*/
var id: Long
/**
* Model deletion checker.
*/
var markedForDeletion: Boolean
}

View file

@ -1,93 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.data.sync
import android.os.Bundle
import android.util.Log
import com.nextcloud.talk.data.changeListVersion.SyncableModel
import kotlin.coroutines.cancellation.CancellationException
/**
* Interface marker for a class that manages synchronization between local data and a remote
* source for a [Syncable].
*/
interface Synchronizer {
// TODO include any other helper functions here that the Synchronizer needs
/**
* Syntactic sugar to call [Syncable.syncWith] while omitting the synchronizer argument
*/
suspend fun Syncable.sync(bundle: Bundle) = this@sync.syncWith(bundle, this@Synchronizer)
}
/**
* Interface marker for a class that is synchronized with a remote source. Syncing must not be
* performed concurrently and it is the [Synchronizer]'s responsibility to ensure this.
*/
interface Syncable {
/**
* Synchronizes the local database backing the repository with the network.
* Takes in a [bundle] to retrieve other metadata needed
*
* Returns if the sync was successful or not.
*/
suspend fun syncWith(bundle: Bundle, synchronizer: Synchronizer): Boolean
}
/**
* Attempts [block], returning a successful [Result] if it succeeds, otherwise a [Result.Failure]
* taking care not to break structured concurrency
*/
private suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> =
try {
Result.success(block())
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (exception: Exception) {
Log.e(
"suspendRunCatching",
"Failed to evaluate a suspendRunCatchingBlock. Returning failure Result",
exception
)
Result.failure(exception)
}
/**
* Utility function for syncing a repository with the network.
* [modelFetcher] Fetches the change list for the model
* [versionUpdater] Updates the version after a successful sync
* [modelDeleter] Deletes models by consuming the ids of the models that have been deleted.
* [modelUpdater] Updates models by consuming the ids of the models that have changed.
*
* Note that the blocks defined above are never run concurrently, and the [Synchronizer]
* implementation must guarantee this.
*/
suspend fun Synchronizer.changeListSync(
modelFetcher: suspend () -> List<SyncableModel>,
versionUpdater: (Long) -> Unit,
modelDeleter: suspend (List<Long>) -> Unit,
modelUpdater: suspend (List<SyncableModel>) -> Unit
) = suspendRunCatching {
// Fetch the change list since last sync (akin to a git fetch)
val changeList = modelFetcher()
if (changeList.isEmpty()) return@suspendRunCatching true
// Splits the models marked for deletion from the ones that are updated or new
val (deleted, updated) = changeList.partition(SyncableModel::markedForDeletion)
// Delete models that have been deleted server-side
modelDeleter(deleted.map(SyncableModel::id))
// Using the fetch list, pull down and upsert the changes (akin to a git pull)
modelUpdater(updated)
// Update the last synced version (akin to updating local git HEAD)
val latestVersion = changeList.last().id
versionUpdater(latestVersion)
}.isSuccess

View file

@ -7,7 +7,6 @@
*/ */
package com.nextcloud.talk.models.domain package com.nextcloud.talk.models.domain
import com.nextcloud.talk.data.changeListVersion.SyncableModel
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.chat.ChatMessageJson import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.Conversation
@ -62,9 +61,7 @@ class ConversationModel(
var recordingConsentRequired: Int = 0, var recordingConsentRequired: Int = 0,
var remoteServer: String? = null, var remoteServer: String? = null,
var remoteToken: String? = null, var remoteToken: String? = null,
override var id: Long = roomId?.toLong() ?: 0, ) {
override var markedForDeletion: Boolean = false
) : SyncableModel {
companion object { companion object {
fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel { fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel {

View file

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com> * SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@ -10,7 +10,6 @@ package com.nextcloud.talk.models.json.chat
import android.os.Parcelable import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.data.changeListVersion.SyncableModel
import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType
import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize

View file

@ -12,10 +12,9 @@ package com.nextcloud.talk.models.json.conversations
import android.os.Parcelable import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.data.changeListVersion.SyncableModel
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.models.json.converters.ConversationObjectTypeConverter import com.nextcloud.talk.models.json.converters.ConversationObjectTypeConverter
import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter
import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter
@ -156,10 +155,7 @@ data class Conversation(
@JsonField(name = ["remoteToken"]) @JsonField(name = ["remoteToken"])
var remoteToken: String? = null, var remoteToken: String? = null,
override var id: Long = 0, ) : Parcelable {
override var markedForDeletion: Boolean = false
) : Parcelable, SyncableModel {
@Deprecated("Use ConversationUtil") @Deprecated("Use ConversationUtil")
val isPublic: Boolean val isPublic: Boolean
get() = ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == type get() = ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == type