diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/syncstatus/SpaceSyncStatusScreen.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/syncstatus/SpaceSyncStatusScreen.kt index 82f8836ef6..2b2a823c64 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/syncstatus/SpaceSyncStatusScreen.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/syncstatus/SpaceSyncStatusScreen.kt @@ -4,7 +4,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -231,7 +230,7 @@ private fun SpaceSyncStatusItem( .padding(start = 16.dp) .align(Alignment.CenterStart), painter = networkCardSettings.icon, - contentDescription = "dfas", + contentDescription = "sync status icon", alpha = networkCardSettings.alpha ) Column( @@ -274,13 +273,14 @@ private fun getP2PCardSettings( P2PStatus.NOT_CONNECTED -> { CardSettings( icon = painterResource(R.drawable.ic_sync_p2p_default), - mainText = stringResource(id = R.string.sync_status_p2p_connecting), + mainText = stringResource(id = R.string.sync_status_p2p), + secondaryText = stringResource(id = R.string.sync_status_p2p_not_connected) ) } P2PStatus.NOT_POSSIBLE, P2PStatus.RESTRICTED -> { CardSettings( - icon = painterResource(R.drawable.ic_sync_p2p_error), + icon = painterResource(R.drawable.ic_sync_p2p_default), mainText = stringResource(id = R.string.sync_status_p2p), secondaryText = stringResource(id = R.string.sync_status_p2p_disabled) ) @@ -307,54 +307,78 @@ private fun getNetworkCardSettings( error: SpaceSyncError, syncingObjectsCounter: Long ): CardSettings { - return when (network) { - SpaceSyncNetwork.ANYTYPE -> when (syncStatus) { - SpaceSyncStatus.SYNCED -> { - CardSettings( - icon = painterResource(R.drawable.ic_sync_net_connected), - mainText = stringResource(id = R.string.sync_status_anytype_network), - secondaryText = stringResource(id = R.string.sync_status_anytype_end_to_end) - ) - } - SpaceSyncStatus.SYNCING -> { - CardSettings( - icon = painterResource(R.drawable.ic_sync_net_connected), - alpha = 0.5f, - withAnimation = true, - mainText = stringResource(id = R.string.sync_status_anytype_network), - secondaryText = pluralStringResource( - id = R.plurals.sync_status_network_items, - count = syncingObjectsCounter.toInt(), - formatArgs = arrayOf(syncingObjectsCounter.toInt()) - ) - ) - } - SpaceSyncStatus.ERROR -> { - val errorText = getErrorText(error) - CardSettings( + when (network) { + SpaceSyncNetwork.ANYTYPE -> { + + if (error != SpaceSyncError.NULL) { + return CardSettings( icon = painterResource(R.drawable.ic_sync_net_error), mainText = stringResource(id = R.string.sync_status_anytype_network), - secondaryText = stringResource(id = errorText) + secondaryText = stringResource(id = getErrorText(error)) ) } - SpaceSyncStatus.OFFLINE -> { - CardSettings( - icon = painterResource(R.drawable.ic_sync_net_default), - mainText = stringResource(id = R.string.sync_status_anytype_network), - secondaryText = stringResource(id = R.string.sync_status_anytype_network_no_connecting) - ) - } - SpaceSyncStatus.NETWORK_UPDATE_NEEDED -> { - CardSettings( - icon = painterResource(R.drawable.ic_sync_limitations), - mainText = stringResource(id = R.string.sync_status_anytype_network), - secondaryText = stringResource(id = R.string.sync_status_anytype_sync_slow) - ) + + return when (syncStatus) { + SpaceSyncStatus.SYNCED -> { + CardSettings( + icon = painterResource(R.drawable.ic_sync_net_connected), + mainText = stringResource(id = R.string.sync_status_anytype_network), + secondaryText = stringResource(id = R.string.sync_status_anytype_end_to_end) + ) + } + + SpaceSyncStatus.SYNCING -> { + CardSettings( + icon = painterResource(R.drawable.ic_sync_net_connected), + alpha = 0.5f, + withAnimation = true, + mainText = stringResource(id = R.string.sync_status_anytype_network), + secondaryText = pluralStringResource( + id = R.plurals.sync_status_network_items, + count = syncingObjectsCounter.toInt(), + formatArgs = arrayOf(syncingObjectsCounter.toInt()) + ) + ) + } + + SpaceSyncStatus.ERROR -> { + val errorText = getErrorText(error) + CardSettings( + icon = painterResource(R.drawable.ic_sync_net_error), + mainText = stringResource(id = R.string.sync_status_anytype_network), + secondaryText = stringResource(id = errorText) + ) + } + + SpaceSyncStatus.OFFLINE -> { + CardSettings( + icon = painterResource(R.drawable.ic_sync_net_default), + mainText = stringResource(id = R.string.sync_status_anytype_network), + secondaryText = stringResource(id = R.string.sync_status_anytype_network_no_connecting) + ) + } + + SpaceSyncStatus.NETWORK_UPDATE_NEEDED -> { + CardSettings( + icon = painterResource(R.drawable.ic_sync_limitations), + mainText = stringResource(id = R.string.sync_status_anytype_network), + secondaryText = stringResource(id = R.string.sync_status_anytype_sync_slow) + ) + } } } SpaceSyncNetwork.SELF_HOST -> { - when (syncStatus) { + + if (error != SpaceSyncError.NULL) { + return CardSettings( + icon = painterResource(R.drawable.ic_sync_net_error), + mainText = stringResource(id = R.string.sync_status_self_host), + secondaryText = stringResource(id = getErrorText(error)) + ) + } + + return when (syncStatus) { SpaceSyncStatus.SYNCED -> { CardSettings( icon = painterResource(R.drawable.ic_sync_self_connected), @@ -402,8 +426,18 @@ private fun getNetworkCardSettings( } } } + SpaceSyncNetwork.LOCAL_ONLY -> { - CardSettings( + + if (error != SpaceSyncError.NULL) { + return CardSettings( + icon = painterResource(R.drawable.ic_sync_net_error), + mainText = stringResource(id = R.string.sync_status_local_only_title), + secondaryText = stringResource(id = getErrorText(error)) + ) + } + + return CardSettings( icon = painterResource(R.drawable.ic_sync_local_only), mainText = stringResource(id = R.string.sync_status_local_only_title), secondaryText = stringResource(id = R.string.sync_status_data_backup) @@ -557,7 +591,7 @@ fun SpaceSyncStatusPreview10() { P2PStatusItem(p2pStatus = p2pStatus) } -@Preview(name = "P2PNotPossible", showBackground = true) +@Preview(name = "P2PNotPossible and P2PRestricted", showBackground = true) @Composable fun SpaceSyncStatusPreview11() { val p2pStatus = P2PStatusUpdate.Update( diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 4e87dfd3e2..3ec9c4bb93 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1721,9 +1721,9 @@ Please provide specific details of your needs here. %1$d device connected %1$d devices connected - P2P Connecting... - Disabled + Connection not possible Restricted. Check device settings. + Not connected Anytype Network End-to-end encrypted diff --git a/middleware/build.gradle b/middleware/build.gradle index eb6aecc0e5..1dcdfa8487 100644 --- a/middleware/build.gradle +++ b/middleware/build.gradle @@ -23,6 +23,8 @@ dependencies { testImplementation libs.junit testImplementation libs.kotlinTest testImplementation libs.mockitoKotlin + testImplementation libs.coroutineTesting + testImplementation libs.turbine } android { diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/SyncAndP2PChannelContainer.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/SyncAndP2PChannelContainer.kt index 8fc586c1fc..8541c8e8c7 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/SyncAndP2PChannelContainer.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/SyncAndP2PChannelContainer.kt @@ -25,8 +25,8 @@ class SyncAndP2PStatusEventsStoreImpl( private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : SyncAndP2PStatusEventsStore { - private val _p2pStatus = MutableStateFlow>(mutableMapOf()) - private val _syncStatus = MutableStateFlow>(mutableMapOf()) + private val _p2pStatus = MutableStateFlow>(emptyMap()) + private val _syncStatus = MutableStateFlow>(emptyMap()) override val p2pStatus: Flow> get() = _p2pStatus override val syncStatus: Flow> get() = _syncStatus @@ -62,8 +62,7 @@ class SyncAndP2PStatusEventsStoreImpl( devicesCounter = update.devicesCounter ) _p2pStatus.update { currentMap -> - currentMap[update.spaceId] = p2pUpdate - currentMap + currentMap + (update.spaceId to p2pUpdate) } } @@ -77,8 +76,7 @@ class SyncAndP2PStatusEventsStoreImpl( ) _syncStatus.update { currentMap -> - currentMap[update.id] = syncUpdate - currentMap + currentMap + (update.id to syncUpdate) } } } \ No newline at end of file diff --git a/middleware/src/test/java/com/anytypeio/anytype/middleware/interactor/EventHandlerChannelImplTest.kt b/middleware/src/test/java/com/anytypeio/anytype/middleware/interactor/EventHandlerChannelImplTest.kt new file mode 100644 index 0000000000..5017169df1 --- /dev/null +++ b/middleware/src/test/java/com/anytypeio/anytype/middleware/interactor/EventHandlerChannelImplTest.kt @@ -0,0 +1,50 @@ +package com.anytypeio.anytype.middleware.interactor + +import app.cash.turbine.test +import app.cash.turbine.turbineScope +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +class EventHandlerChannelImplTest { + + @Test + fun `channel should receive events`() = runTest { + + turbineScope { + val event1 = anytype.Event( + messages = listOf( + anytype.Event.Message( + p2pStatusUpdate = anytype.Event.P2PStatus.Update( + spaceId = "spaceId1", + status = anytype.Event.P2PStatus.Status.Connected, + devicesCounter = 145 + ) + ) + ) + ) + + val event2 = anytype.Event( + messages = listOf( + anytype.Event.Message( + spaceSyncStatusUpdate = anytype.Event.Space.SyncStatus.Update( + id = "id1", + status = anytype.Event.Space.Status.Syncing, + network = anytype.Event.Space.Network.Anytype, + syncingObjectsCounter = 999 + ) + ) + ) + ) + + val eventHandlerChannelImpl = EventHandlerChannelImpl() + + eventHandlerChannelImpl.flow().test { + eventHandlerChannelImpl.emit(event1) + assertEquals(event1, awaitItem()) + eventHandlerChannelImpl.emit(event2) + assertEquals(event2, awaitItem()) + } + } + } +} \ No newline at end of file diff --git a/middleware/src/test/java/com/anytypeio/anytype/middleware/interactor/SyncAndP2PStatusEventsStoreImplTest.kt b/middleware/src/test/java/com/anytypeio/anytype/middleware/interactor/SyncAndP2PStatusEventsStoreImplTest.kt new file mode 100644 index 0000000000..f031e46a46 --- /dev/null +++ b/middleware/src/test/java/com/anytypeio/anytype/middleware/interactor/SyncAndP2PStatusEventsStoreImplTest.kt @@ -0,0 +1,297 @@ +package com.anytypeio.anytype.middleware.interactor + +import app.cash.turbine.test +import app.cash.turbine.turbineScope +import com.anytypeio.anytype.core_models.multiplayer.P2PStatus +import com.anytypeio.anytype.core_models.multiplayer.P2PStatusUpdate +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncError +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncNetwork +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncStatus +import com.anytypeio.anytype.core_models.multiplayer.SpaceSyncUpdate +import kotlin.test.assertEquals +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + + +class SyncAndP2PStatusEventsStoreImplTest { + + private val dispatcher = StandardTestDispatcher(name = "Default test dispatcher") + + private val spaceId1 = "spaceId1" + private val spaceId2 = "spaceId2" + private val spaceId3 = "spaceId3" + + private lateinit var channel: EventHandlerChannelImpl + + @Before + fun setUp() { + channel = EventHandlerChannelImpl() + } + + @Test + fun `should update spaces p2p statuses`() = runTest(dispatcher) { + + turbineScope { + + val store = SyncAndP2PStatusEventsStoreImpl( + channel = channel, + dispatcher = dispatcher, + scope = backgroundScope + ) + + val initialEvent = anytype.Event( + messages = listOf( + anytype.Event.Message( + p2pStatusUpdate = anytype.Event.P2PStatus.Update( + spaceId = spaceId1, + ) + ), + anytype.Event.Message( + p2pStatusUpdate = anytype.Event.P2PStatus.Update( + spaceId = spaceId2, + status = anytype.Event.P2PStatus.Status.Connected, + devicesCounter = 145 + ) + ) + ) + ) + + val event1 = anytype.Event( + messages = listOf( + anytype.Event.Message( + p2pStatusUpdate = anytype.Event.P2PStatus.Update( + spaceId = spaceId2, + status = anytype.Event.P2PStatus.Status.Connected, + devicesCounter = 233 + ) + ) + ) + ) + + val event2 = anytype.Event( + messages = listOf( + anytype.Event.Message( + p2pStatusUpdate = anytype.Event.P2PStatus.Update( + spaceId = spaceId1, + status = anytype.Event.P2PStatus.Status.NotPossible + ) + ) + ) + ) + + store.start() + + dispatcher.scheduler.advanceTimeBy(100) + + store.p2pStatus.test { + val firstItem = awaitItem() + assertEquals(mapOf(), firstItem) + channel.emit(initialEvent) + assertEquals( + expected = mapOf( + spaceId1 to P2PStatusUpdate.Update( + spaceId = spaceId1, + status = P2PStatus.NOT_CONNECTED, + devicesCounter = 0 + ) + ), + actual = awaitItem() + ) + assertEquals( + expected = mapOf( + spaceId1 to P2PStatusUpdate.Update( + spaceId = spaceId1, + status = P2PStatus.NOT_CONNECTED, + devicesCounter = 0 + ), + spaceId2 to P2PStatusUpdate.Update( + spaceId = spaceId2, + status = P2PStatus.CONNECTED, + devicesCounter = 145 + ) + ), + actual = awaitItem() + ) + channel.emit(event1) + assertEquals( + expected = mapOf( + spaceId1 to P2PStatusUpdate.Update( + spaceId = spaceId1, + status = P2PStatus.NOT_CONNECTED, + devicesCounter = 0 + ), + spaceId2 to P2PStatusUpdate.Update( + spaceId = spaceId2, + status = P2PStatus.CONNECTED, + devicesCounter = 233 + ) + ), + actual = awaitItem() + ) + channel.emit(event2) + assertEquals( + expected = mapOf( + spaceId1 to P2PStatusUpdate.Update( + spaceId = spaceId1, + status = P2PStatus.NOT_POSSIBLE, + devicesCounter = 0 + ), + spaceId2 to P2PStatusUpdate.Update( + spaceId = spaceId2, + status = P2PStatus.CONNECTED, + devicesCounter = 233 + ) + ), + actual = awaitItem() + ) + } + } + } + + @Test + fun `should update spaces sync statuses`() = runTest(dispatcher) { + turbineScope { + + val store = SyncAndP2PStatusEventsStoreImpl( + channel = channel, + dispatcher = dispatcher, + scope = backgroundScope + ) + + val initialEvent = anytype.Event( + messages = listOf( + anytype.Event.Message( + spaceSyncStatusUpdate = anytype.Event.Space.SyncStatus.Update( + id = spaceId1, + status = anytype.Event.Space.Status.Offline, + network = anytype.Event.Space.Network.Anytype + ) + ), + anytype.Event.Message( + spaceSyncStatusUpdate = anytype.Event.Space.SyncStatus.Update( + id = spaceId2, + status = anytype.Event.Space.Status.Syncing, + network = anytype.Event.Space.Network.SelfHost + ) + ) + ) + ) + + val event1 = anytype.Event( + messages = listOf( + anytype.Event.Message( + spaceSyncStatusUpdate = anytype.Event.Space.SyncStatus.Update( + id = spaceId2, + status = anytype.Event.Space.Status.Error, + network = anytype.Event.Space.Network.SelfHost, + error = anytype.Event.Space.SyncError.StorageLimitExceed + ) + ), + anytype.Event.Message( + spaceSyncStatusUpdate = anytype.Event.Space.SyncStatus.Update( + id = spaceId3, + status = anytype.Event.Space.Status.Synced, + network = anytype.Event.Space.Network.Anytype, + syncingObjectsCounter = 2345 + ) + ) + ) + ) + + store.start() + + dispatcher.scheduler.advanceTimeBy(100) + + store.syncStatus.test { + val firstItem = awaitItem() + assertEquals(mapOf(), firstItem) + + channel.emit(initialEvent) + + assertEquals( + expected = mapOf( + spaceId1 to SpaceSyncUpdate.Update( + id = spaceId1, + status = SpaceSyncStatus.OFFLINE, + network = SpaceSyncNetwork.ANYTYPE, + syncingObjectsCounter = 0, + error = SpaceSyncError.NULL + ) + ), + actual = awaitItem() + ) + + assertEquals( + expected = mapOf( + spaceId1 to SpaceSyncUpdate.Update( + id = spaceId1, + status = SpaceSyncStatus.OFFLINE, + network = SpaceSyncNetwork.ANYTYPE, + syncingObjectsCounter = 0, + error = SpaceSyncError.NULL + ), + spaceId2 to SpaceSyncUpdate.Update( + id = spaceId2, + status = SpaceSyncStatus.SYNCING, + network = SpaceSyncNetwork.SELF_HOST, + syncingObjectsCounter = 0, + error = SpaceSyncError.NULL + ) + ), + actual = awaitItem() + ) + + channel.emit(event1) + + assertEquals( + expected = mapOf( + spaceId1 to SpaceSyncUpdate.Update( + id = spaceId1, + status = SpaceSyncStatus.OFFLINE, + network = SpaceSyncNetwork.ANYTYPE, + syncingObjectsCounter = 0, + error = SpaceSyncError.NULL + ), + spaceId2 to SpaceSyncUpdate.Update( + id = spaceId2, + status = SpaceSyncStatus.ERROR, + network = SpaceSyncNetwork.SELF_HOST, + syncingObjectsCounter = 0, + error = SpaceSyncError.STORAGE_LIMIT_EXCEED + ) + ), + actual = awaitItem() + ) + + assertEquals( + expected = mapOf( + spaceId1 to SpaceSyncUpdate.Update( + id = spaceId1, + status = SpaceSyncStatus.OFFLINE, + network = SpaceSyncNetwork.ANYTYPE, + syncingObjectsCounter = 0, + error = SpaceSyncError.NULL + ), + spaceId2 to SpaceSyncUpdate.Update( + id = spaceId2, + status = SpaceSyncStatus.ERROR, + network = SpaceSyncNetwork.SELF_HOST, + syncingObjectsCounter = 0, + error = SpaceSyncError.STORAGE_LIMIT_EXCEED + ), + spaceId3 to SpaceSyncUpdate.Update( + id = spaceId3, + status = SpaceSyncStatus.SYNCED, + network = SpaceSyncNetwork.ANYTYPE, + syncingObjectsCounter = 2345, + error = SpaceSyncError.NULL + ) + ), + actual = awaitItem() + ) + } + } + } +} \ No newline at end of file