mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 11:05:25 +00:00
Move config sync task into dedicated class
This commit is contained in:
parent
79db7bdc5c
commit
8c9dfdf5cd
@ -41,7 +41,8 @@ import com.squareup.phrase.Phrase;
|
|||||||
import org.conscrypt.Conscrypt;
|
import org.conscrypt.Conscrypt;
|
||||||
import org.session.libsession.database.MessageDataProvider;
|
import org.session.libsession.database.MessageDataProvider;
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||||
import org.session.libsession.messaging.configs.ConfigUploader;
|
import org.thoughtcrime.securesms.configs.ConfigToDatabaseSync;
|
||||||
|
import org.thoughtcrime.securesms.configs.ConfigUploader;
|
||||||
import org.session.libsession.messaging.groups.GroupManagerV2;
|
import org.session.libsession.messaging.groups.GroupManagerV2;
|
||||||
import org.session.libsession.messaging.groups.RemoveGroupMemberHandler;
|
import org.session.libsession.messaging.groups.RemoveGroupMemberHandler;
|
||||||
import org.session.libsession.messaging.notifications.TokenFetcher;
|
import org.session.libsession.messaging.notifications.TokenFetcher;
|
||||||
@ -160,8 +161,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
@Inject SSKEnvironment.ProfileManagerProtocol profileManager;
|
@Inject SSKEnvironment.ProfileManagerProtocol profileManager;
|
||||||
CallMessageProcessor callMessageProcessor;
|
CallMessageProcessor callMessageProcessor;
|
||||||
MessagingModuleConfiguration messagingModuleConfiguration;
|
MessagingModuleConfiguration messagingModuleConfiguration;
|
||||||
@Inject
|
@Inject ConfigUploader configUploader;
|
||||||
ConfigUploader configUploader;
|
@Inject ConfigToDatabaseSync configToDatabaseSync;
|
||||||
@Inject RemoveGroupMemberHandler removeGroupMemberHandler;
|
@Inject RemoveGroupMemberHandler removeGroupMemberHandler;
|
||||||
@Inject SnodeClock snodeClock;
|
@Inject SnodeClock snodeClock;
|
||||||
|
|
||||||
@ -267,10 +268,11 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
|
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
|
||||||
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
|
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
|
||||||
|
|
||||||
|
snodeClock.start();
|
||||||
pushRegistrationHandler.run();
|
pushRegistrationHandler.run();
|
||||||
configUploader.start();
|
configUploader.start();
|
||||||
|
configToDatabaseSync.start();
|
||||||
removeGroupMemberHandler.start();
|
removeGroupMemberHandler.start();
|
||||||
snodeClock.start();
|
|
||||||
|
|
||||||
// add our shortcut debug menu if we are not in a release build
|
// add our shortcut debug menu if we are not in a release build
|
||||||
if (BuildConfig.BUILD_TYPE != "release") {
|
if (BuildConfig.BUILD_TYPE != "release") {
|
||||||
|
@ -0,0 +1,325 @@
|
|||||||
|
package org.thoughtcrime.securesms.configs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
|
||||||
|
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
|
||||||
|
import network.loki.messenger.libsession_util.ReadableContacts
|
||||||
|
import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig
|
||||||
|
import network.loki.messenger.libsession_util.ReadableGroupInfoConfig
|
||||||
|
import network.loki.messenger.libsession_util.ReadableUserGroupsConfig
|
||||||
|
import network.loki.messenger.libsession_util.ReadableUserProfile
|
||||||
|
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
|
||||||
|
import network.loki.messenger.libsession_util.util.Conversation
|
||||||
|
import network.loki.messenger.libsession_util.util.UserPic
|
||||||
|
import network.loki.messenger.libsession_util.util.afterSend
|
||||||
|
import org.session.libsession.database.StorageProtocol
|
||||||
|
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
|
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.LegacyClosedGroupPollerV2
|
||||||
|
import org.session.libsession.snode.SnodeClock
|
||||||
|
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||||
|
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||||
|
import org.session.libsession.utilities.ConfigUpdateNotification
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||||
|
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||||
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
|
import org.session.libsignal.utilities.AccountId
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.database.MmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.PollerFactory
|
||||||
|
import org.thoughtcrime.securesms.groups.ClosedGroupManager
|
||||||
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val TAG = "ConfigToDatabaseSync"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for syncing config system's data into the database.
|
||||||
|
*
|
||||||
|
* It does so by listening to the [ConfigFactoryProtocol.configUpdateNotifications] and updating the database accordingly.
|
||||||
|
*
|
||||||
|
* @see ConfigUploader For upload config system data into swarm automagically.
|
||||||
|
*/
|
||||||
|
class ConfigToDatabaseSync @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val configFactory: ConfigFactoryProtocol,
|
||||||
|
private val storage: StorageProtocol,
|
||||||
|
private val threadDatabase: ThreadDatabase,
|
||||||
|
private val recipientDatabase: RecipientDatabase,
|
||||||
|
private val mmsDatabase: MmsDatabase,
|
||||||
|
private val pollerFactory: PollerFactory,
|
||||||
|
private val clock: SnodeClock,
|
||||||
|
) {
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
require(job == null) { "Already started" }
|
||||||
|
|
||||||
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
job = GlobalScope.launch {
|
||||||
|
val groupMutex = hashMapOf<AccountId, Mutex>()
|
||||||
|
val userMutex = Mutex()
|
||||||
|
|
||||||
|
configFactory.configUpdateNotifications.collect { notification ->
|
||||||
|
when (notification) {
|
||||||
|
is ConfigUpdateNotification.UserConfigs -> {
|
||||||
|
launch {
|
||||||
|
userMutex.withLock {
|
||||||
|
syncUserConfigs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ConfigUpdateNotification.GroupConfigsUpdated -> {
|
||||||
|
val groupId = notification.groupId
|
||||||
|
val mutex = groupMutex.getOrPut(groupId) { Mutex() }
|
||||||
|
|
||||||
|
launch {
|
||||||
|
mutex.withLock {
|
||||||
|
syncGroupConfigs(groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ConfigUpdateNotification.GroupConfigsDeleted -> {
|
||||||
|
groupMutex.remove(notification.groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncGroupConfigs(groupId: AccountId) {
|
||||||
|
configFactory.withGroupConfigs(groupId) {
|
||||||
|
updateGroup(it.groupInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncUserConfigs() {
|
||||||
|
val messageTimestamp = clock.currentTimeMills()
|
||||||
|
configFactory.withUserConfigs { configs ->
|
||||||
|
updateUser(configs.userProfile, messageTimestamp)
|
||||||
|
updateUserGroups(configs.userGroups, messageTimestamp)
|
||||||
|
updateContacts(configs.contacts, messageTimestamp)
|
||||||
|
updateConvoVolatile(configs.convoInfoVolatile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUser(userProfile: ReadableUserProfile, messageTimestamp: Long) {
|
||||||
|
val userPublicKey = storage.getUserPublicKey() ?: return
|
||||||
|
// would love to get rid of recipient and context from this
|
||||||
|
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
|
||||||
|
|
||||||
|
// Update profile name
|
||||||
|
val name = userProfile.getName() ?: return
|
||||||
|
val userPic = userProfile.getPic()
|
||||||
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
|
|
||||||
|
name.takeUnless { it.isEmpty() }?.truncate(NAME_PADDED_LENGTH)?.let {
|
||||||
|
TextSecurePreferences.setProfileName(context, it)
|
||||||
|
profileManager.setName(context, recipient, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update profile picture
|
||||||
|
if (userPic == UserPic.DEFAULT) {
|
||||||
|
storage.clearUserPic()
|
||||||
|
} else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty()
|
||||||
|
&& TextSecurePreferences.getProfilePictureURL(context) != userPic.url
|
||||||
|
) {
|
||||||
|
storage.setUserProfilePicture(userPic.url, userPic.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) {
|
||||||
|
// delete nts thread if needed
|
||||||
|
val ourThread = storage.getThreadId(recipient) ?: return
|
||||||
|
storage.deleteConversation(ourThread)
|
||||||
|
} else {
|
||||||
|
// create note to self thread if needed (?)
|
||||||
|
val address = recipient.address
|
||||||
|
val ourThread = storage.getThreadId(address) ?: storage.getOrCreateThreadIdFor(address).also {
|
||||||
|
storage.setThreadDate(it, 0)
|
||||||
|
}
|
||||||
|
threadDatabase.setHasSent(ourThread, true)
|
||||||
|
storage.setPinned(ourThread, userProfile.getNtsPriority() > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set or reset the shared library to use latest expiration config
|
||||||
|
storage.getThreadId(recipient)?.let {
|
||||||
|
storage.setExpirationConfiguration(
|
||||||
|
storage.getExpirationConfiguration(it)?.takeIf { it.updatedTimestampMs > messageTimestamp } ?:
|
||||||
|
ExpirationConfiguration(it, userProfile.getNtsExpiry(), messageTimestamp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateGroup(groupInfoConfig: ReadableGroupInfoConfig) {
|
||||||
|
val threadId = storage.getThreadId(fromSerialized(groupInfoConfig.id().hexString)) ?: return
|
||||||
|
val recipient = storage.getRecipientForThread(threadId) ?: return
|
||||||
|
recipientDatabase.setProfileName(recipient, groupInfoConfig.getName())
|
||||||
|
groupInfoConfig.getDeleteBefore()?.let { removeBefore ->
|
||||||
|
storage.trimThreadBefore(threadId, removeBefore)
|
||||||
|
}
|
||||||
|
groupInfoConfig.getDeleteAttachmentsBefore()?.let { removeAttachmentsBefore ->
|
||||||
|
mmsDatabase.deleteMessagesInThreadBeforeDate(threadId, removeAttachmentsBefore, onlyMedia = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateContacts(contacts: ReadableContacts, messageTimestamp: Long) {
|
||||||
|
val extracted = contacts.all().toList()
|
||||||
|
storage.addLibSessionContacts(extracted, messageTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUserGroups(userGroups: ReadableUserGroupsConfig, messageTimestamp: Long) {
|
||||||
|
val localUserPublicKey = storage.getUserPublicKey() ?: return Log.w(
|
||||||
|
"Loki",
|
||||||
|
"No user public key when trying to update user groups from config"
|
||||||
|
)
|
||||||
|
val communities = userGroups.allCommunityInfo()
|
||||||
|
val lgc = userGroups.allLegacyGroupInfo()
|
||||||
|
val allOpenGroups = storage.getAllOpenGroups()
|
||||||
|
val toDeleteCommunities = allOpenGroups.filter {
|
||||||
|
Conversation.Community(BaseCommunityInfo(it.value.server, it.value.room, it.value.publicKey), 0, false).baseCommunityInfo.fullUrl() !in communities.map { it.community.fullUrl() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val existingCommunities: Map<Long, OpenGroup> = allOpenGroups.filterKeys { it !in toDeleteCommunities.keys }
|
||||||
|
val toAddCommunities = communities.filter { it.community.fullUrl() !in existingCommunities.map { it.value.joinURL } }
|
||||||
|
val existingJoinUrls = existingCommunities.values.map { it.joinURL }
|
||||||
|
|
||||||
|
val existingLegacyClosedGroups = storage.getAllGroups(includeInactive = true).filter { it.isLegacyClosedGroup }
|
||||||
|
val lgcIds = lgc.map { it.accountId }
|
||||||
|
val toDeleteClosedGroups = existingLegacyClosedGroups.filter { group ->
|
||||||
|
GroupUtil.doubleDecodeGroupId(group.encodedId) !in lgcIds
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the ones which are not listed in the config
|
||||||
|
toDeleteCommunities.values.forEach { openGroup ->
|
||||||
|
OpenGroupManager.delete(openGroup.server, openGroup.room, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
toDeleteClosedGroups.forEach { deleteGroup ->
|
||||||
|
val threadId = storage.getThreadId(deleteGroup.encodedId)
|
||||||
|
if (threadId != null) {
|
||||||
|
ClosedGroupManager.silentlyRemoveGroup(context,threadId,
|
||||||
|
GroupUtil.doubleDecodeGroupId(deleteGroup.encodedId), deleteGroup.encodedId, localUserPublicKey, delete = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toAddCommunities.forEach { toAddCommunity ->
|
||||||
|
val joinUrl = toAddCommunity.community.fullUrl()
|
||||||
|
if (!storage.hasBackgroundGroupAddJob(joinUrl)) {
|
||||||
|
JobQueue.shared.add(BackgroundGroupAddJob(joinUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (groupInfo in communities) {
|
||||||
|
val groupBaseCommunity = groupInfo.community
|
||||||
|
if (groupBaseCommunity.fullUrl() in existingJoinUrls) {
|
||||||
|
// add it
|
||||||
|
val (threadId, _) = existingCommunities.entries.first { (_, v) -> v.joinURL == groupInfo.community.fullUrl() }
|
||||||
|
threadDatabase.setPinned(threadId, groupInfo.priority == PRIORITY_PINNED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newClosedGroups = userGroups.allClosedGroupInfo()
|
||||||
|
for (closedGroup in newClosedGroups) {
|
||||||
|
val recipient = Recipient.from(context, fromSerialized(closedGroup.groupAccountId.hexString), false)
|
||||||
|
storage.setRecipientApprovedMe(recipient, true)
|
||||||
|
storage.setRecipientApproved(recipient, !closedGroup.invited)
|
||||||
|
val threadId = storage.getOrCreateThreadIdFor(recipient.address)
|
||||||
|
storage.setPinned(threadId, closedGroup.priority == PRIORITY_PINNED)
|
||||||
|
if (!closedGroup.invited) {
|
||||||
|
pollerFactory.pollerFor(closedGroup.groupAccountId)?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (group in lgc) {
|
||||||
|
val groupId = GroupUtil.doubleEncodeGroupID(group.accountId)
|
||||||
|
val existingGroup = existingLegacyClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.accountId }
|
||||||
|
val existingThread = existingGroup?.let { storage.getThreadId(existingGroup.encodedId) }
|
||||||
|
if (existingGroup != null) {
|
||||||
|
if (group.priority == PRIORITY_HIDDEN && existingThread != null) {
|
||||||
|
ClosedGroupManager.silentlyRemoveGroup(context,existingThread,
|
||||||
|
GroupUtil.doubleDecodeGroupId(existingGroup.encodedId), existingGroup.encodedId, localUserPublicKey, delete = true)
|
||||||
|
} else if (existingThread == null) {
|
||||||
|
Log.w("Loki-DBG", "Existing group had no thread to hide")
|
||||||
|
} else {
|
||||||
|
Log.d("Loki-DBG", "Setting existing group pinned status to ${group.priority}")
|
||||||
|
threadDatabase.setPinned(existingThread, group.priority == PRIORITY_PINNED)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val members = group.members.keys.map { fromSerialized(it) }
|
||||||
|
val admins = group.members.filter { it.value /*admin = true*/ }.keys.map { fromSerialized(it) }
|
||||||
|
val title = group.name
|
||||||
|
val formationTimestamp = (group.joinedAt * 1000L)
|
||||||
|
storage.createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp)
|
||||||
|
storage.setProfileSharing(fromSerialized(groupId), true)
|
||||||
|
// Add the group to the user's set of public keys to poll for
|
||||||
|
storage.addClosedGroupPublicKey(group.accountId)
|
||||||
|
// Store the encryption key pair
|
||||||
|
val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey))
|
||||||
|
storage.addClosedGroupEncryptionKeyPair(keyPair, group.accountId, clock.currentTimeMills())
|
||||||
|
// Notify the PN server
|
||||||
|
PushRegistryV1.subscribeGroup(group.accountId, publicKey = localUserPublicKey)
|
||||||
|
// Notify the user
|
||||||
|
val threadID = storage.getOrCreateThreadIdFor(fromSerialized(groupId))
|
||||||
|
threadDatabase.setDate(threadID, formationTimestamp)
|
||||||
|
|
||||||
|
// Note: Commenting out this line prevents the timestamp of room creation being added to a new closed group,
|
||||||
|
// which in turn allows us to show the `groupNoMessages` control message text.
|
||||||
|
//insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
|
||||||
|
|
||||||
|
// Don't create config group here, it's from a config update
|
||||||
|
// Start polling
|
||||||
|
LegacyClosedGroupPollerV2.shared.startPolling(group.accountId)
|
||||||
|
}
|
||||||
|
storage.getThreadId(fromSerialized(groupId))?.let {
|
||||||
|
storage.setExpirationConfiguration(
|
||||||
|
storage.getExpirationConfiguration(it)?.takeIf { it.updatedTimestampMs > messageTimestamp }
|
||||||
|
?: ExpirationConfiguration(it, afterSend(group.disappearingTimer), messageTimestamp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateConvoVolatile(convos: ReadableConversationVolatileConfig) {
|
||||||
|
val extracted = convos.all().filterNotNull()
|
||||||
|
for (conversation in extracted) {
|
||||||
|
val threadId = when (conversation) {
|
||||||
|
is Conversation.OneToOne -> storage.getThreadIdFor(conversation.accountId, null, null, createThread = false)
|
||||||
|
is Conversation.LegacyGroup -> storage.getThreadIdFor("", conversation.groupId,null, createThread = false)
|
||||||
|
is Conversation.Community -> storage.getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl.removeSuffix("/")}.${conversation.baseCommunityInfo.room}", createThread = false)
|
||||||
|
is Conversation.ClosedGroup -> storage.getThreadIdFor(conversation.accountId, null, null, createThread = false) // New groups will be managed bia libsession
|
||||||
|
}
|
||||||
|
if (threadId != null) {
|
||||||
|
if (conversation.lastRead > storage.getLastSeen(threadId)) {
|
||||||
|
storage.markConversationAsRead(threadId, conversation.lastRead, force = true)
|
||||||
|
}
|
||||||
|
storage.updateThread(threadId, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate a string to a specified number of bytes
|
||||||
|
*
|
||||||
|
* This could split multi-byte characters/emojis.
|
||||||
|
*/
|
||||||
|
private fun String.truncate(sizeInBytes: Int): String =
|
||||||
|
toByteArray().takeIf { it.size > sizeInBytes }?.take(sizeInBytes)?.toByteArray()?.let(::String) ?: this
|
@ -1,4 +1,4 @@
|
|||||||
package org.session.libsession.messaging.configs
|
package org.thoughtcrime.securesms.configs
|
||||||
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@ -36,6 +36,12 @@ private const val TAG = "ConfigUploader"
|
|||||||
/**
|
/**
|
||||||
* This class is responsible for sending the local config changes to the swarm.
|
* This class is responsible for sending the local config changes to the swarm.
|
||||||
*
|
*
|
||||||
|
* Note: This class is listening ONLY to the config system changes. If you change any local database
|
||||||
|
* data, this class will not be aware of it. You'll need to update the config system
|
||||||
|
* for this class to pick up these changes.
|
||||||
|
*
|
||||||
|
* @see ConfigToDatabaseSync For syncing the config changes to the local database.
|
||||||
|
*
|
||||||
* It does so by listening for changes in the config factory.
|
* It does so by listening for changes in the config factory.
|
||||||
*/
|
*/
|
||||||
class ConfigUploader @Inject constructor(
|
class ConfigUploader @Inject constructor(
|
Loading…
Reference in New Issue
Block a user