Skip to content

Commit 594e551

Browse files
authored
bugfix/do_not_proceed_bootstrap_when_no_connectivity (#359)
1 parent 18c3238 commit 594e551

File tree

16 files changed

+113
-33
lines changed

16 files changed

+113
-33
lines changed

androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/di/AndroidNodeModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ val androidNodeModule = module {
6262

6363
single { AndroidApplicationService.Provider() }
6464

65-
single<ApplicationBootstrapFacade> { NodeApplicationBootstrapFacade(get()) }
65+
single<ApplicationBootstrapFacade> { NodeApplicationBootstrapFacade(get(), get()) }
6666

6767
single<MarketPriceServiceFacade> { NodeMarketPriceServiceFacade(get()) }
6868

androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/presentation/NodeSplashPresenter.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import network.bisq.mobile.presentation.MainPresenter
1111
import network.bisq.mobile.presentation.ui.uicases.startup.SplashPresenter
1212

1313
class NodeSplashPresenter(
14-
mainPresenter: MainPresenter,
14+
private val mainPresenter: MainPresenter,
1515
applicationBootstrapFacade: ApplicationBootstrapFacade,
1616
userProfileService: UserProfileServiceFacade,
1717
userRepository: UserRepository,
@@ -36,7 +36,6 @@ class NodeSplashPresenter(
3636
}
3737

3838
override suspend fun hasConnectivity(): Boolean {
39-
// TODO implement for node
40-
return true
39+
return mainPresenter.isConnected()
4140
}
4241
}

androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/service/bootstrap/NodeApplicationBootstrapFacade.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ package network.bisq.mobile.android.node.service.bootstrap
33
import bisq.application.State
44
import bisq.common.observable.Observable
55
import bisq.common.observable.Pin
6+
import kotlinx.coroutines.Job
7+
import kotlinx.coroutines.launch
68
import network.bisq.mobile.android.node.AndroidApplicationService
79
import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade
10+
import network.bisq.mobile.domain.service.network.ConnectivityService
811
import network.bisq.mobile.i18n.i18n
912

1013
class NodeApplicationBootstrapFacade(
11-
private val applicationService: AndroidApplicationService.Provider
14+
private val applicationService: AndroidApplicationService.Provider,
15+
private val connectivityService: ConnectivityService
1216
) : ApplicationBootstrapFacade() {
1317

14-
// Dependencies
1518
private val applicationServiceState: Observable<State> by lazy { applicationService.state.get() }
19+
private var connectivityJob: Job? = null
1620

17-
// Misc
1821
private var applicationServiceStatePin: Pin? = null
1922

2023
override fun activate() {
@@ -53,8 +56,18 @@ class NodeApplicationBootstrapFacade(
5356
State.APP_INITIALIZED -> {
5457
isActive = true
5558
log.i { "Bootstrap activated" }
56-
setState("splash.applicationServiceState.APP_INITIALIZED".i18n())
57-
setProgress(1f)
59+
60+
// Check connectivity before completing bootstrap
61+
if (connectivityService.isConnected()) {
62+
onInitialized()
63+
} else {
64+
setState("bootstrap.noConnectivity".i18n())
65+
setProgress(0.95f) // Not fully complete
66+
connectivityJob = connectivityService.runWhenConnected {
67+
log.d { "Bootstrap: Connectivity restored, completing initialization" }
68+
onInitialized()
69+
}
70+
}
5871
}
5972

6073
State.FAILED -> {
@@ -65,12 +78,19 @@ class NodeApplicationBootstrapFacade(
6578
}
6679
}
6780

81+
private fun onInitialized() {
82+
setState("splash.applicationServiceState.APP_INITIALIZED".i18n())
83+
setProgress(1f)
84+
}
85+
6886
private fun onInitializeAppState() {
6987
setState("splash.applicationServiceState.INITIALIZE_APP".i18n())
7088
setProgress(0f)
7189
}
7290

7391
override fun deactivate() {
92+
connectivityJob?.cancel()
93+
connectivityJob = null
7494
applicationServiceStatePin?.unbind()
7595
applicationServiceStatePin = null
7696
isActive = false

androidNode/src/androidMain/kotlin/network/bisq/mobile/android/node/service/network/NodeConnectivityService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class NodeConnectivityService(private val applicationService: AndroidApplication
2020

2121
override fun isConnected(): Boolean {
2222
val connections = currentConnections()
23-
log.d { "Connected peers = $connections" }
23+
log.v { "Connected peers = $connections" }
2424
return connections > 0
2525
}
2626

shared/domain/src/commonMain/kotlin/network/bisq/mobile/domain/service/network/ConnectivityService.kt

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package network.bisq.mobile.domain.service.network
22

33
import kotlinx.coroutines.CoroutineScope
44
import kotlinx.coroutines.Job
5+
import kotlinx.coroutines.SupervisorJob
56
import kotlinx.coroutines.TimeoutCancellationException
67
import kotlinx.coroutines.delay
78
import kotlinx.coroutines.flow.MutableStateFlow
89
import kotlinx.coroutines.flow.StateFlow
910
import kotlinx.coroutines.launch
11+
import kotlinx.coroutines.sync.Mutex
12+
import kotlinx.coroutines.sync.withLock
1013
import kotlinx.coroutines.withTimeout
1114
import network.bisq.mobile.domain.data.IODispatcher
1215
import network.bisq.mobile.domain.service.ServiceFacade
@@ -47,27 +50,20 @@ abstract class ConnectivityService : ServiceFacade() {
4750
CONNECTED
4851
}
4952

50-
private val ioScope = CoroutineScope(IODispatcher)
53+
private val pendingConnectivityBlocks = mutableListOf<suspend () -> Unit>()
54+
private val mutex = Mutex()
55+
5156
private var job: Job? = null
5257
private val _status = MutableStateFlow(ConnectivityStatus.DISCONNECTED)
5358
val status: StateFlow<ConnectivityStatus> = _status
5459

55-
56-
override fun activate() {
57-
super.activate()
58-
}
59-
60-
override fun deactivate() {
61-
super.deactivate()
62-
}
63-
6460
/**
6561
* Starts monitoring connectivity every given period (ms). Default is 10 seconds.
6662
*/
6763
fun startMonitoring(period: Long = PERIOD) {
6864
onStart()
6965
job?.cancel()
70-
job = ioScope.launch(IODispatcher) {
66+
job = launchIO {
7167
while (true) {
7268
checkConnectivity()
7369
delay(period)
@@ -78,16 +74,18 @@ abstract class ConnectivityService : ServiceFacade() {
7874

7975
private suspend fun checkConnectivity() {
8076
try {
81-
// log.d { "Checking connectivity whilst ${_status.value}" }
8277
withTimeout(TIMEOUT) {
83-
val currentStatus = _status.value
84-
when {
85-
!isConnected() -> _status.value = ConnectivityStatus.DISCONNECTED
86-
isSlow() -> _status.value = ConnectivityStatus.SLOW
87-
else -> _status.value = ConnectivityStatus.CONNECTED
78+
val previousStatus = _status.value
79+
_status.value = when {
80+
!isConnected() -> ConnectivityStatus.DISCONNECTED
81+
isSlow() -> ConnectivityStatus.SLOW
82+
else -> ConnectivityStatus.CONNECTED
8883
}
89-
if (currentStatus != _status.value) {
90-
log.d { "Connectivity transition from $currentStatus to ${_status.value}" }
84+
if (previousStatus != _status.value) {
85+
log.d { "Connectivity transition from $previousStatus to ${_status.value}" }
86+
if (previousStatus == ConnectivityStatus.DISCONNECTED) {
87+
runPendingBlocks()
88+
}
9189
}
9290
}
9391
} catch (e: TimeoutCancellationException) {
@@ -99,6 +97,29 @@ abstract class ConnectivityService : ServiceFacade() {
9997
}
10098
}
10199

100+
private fun runPendingBlocks() {
101+
launchIO {
102+
mutex.withLock {
103+
val blocksToExecute = pendingConnectivityBlocks.let {
104+
val blocks = it.toList()
105+
pendingConnectivityBlocks.clear()
106+
blocks
107+
}
108+
109+
if (blocksToExecute.isNotEmpty()) {
110+
log.d { "Executing ${blocksToExecute.size} pending connectivity blocks" }
111+
112+
blocksToExecute.forEach { block ->
113+
// fire&forget: Create a fresh scope for each block
114+
CoroutineScope(IODispatcher + SupervisorJob()).launch {
115+
block()
116+
}
117+
}
118+
}
119+
}
120+
}
121+
}
122+
102123
fun stopMonitoring() {
103124
job?.cancel()
104125
job = null
@@ -114,7 +135,7 @@ abstract class ConnectivityService : ServiceFacade() {
114135
// default nth
115136
}
116137

117-
protected abstract fun isConnected(): Boolean
138+
abstract fun isConnected(): Boolean
118139

119140
/**
120141
* Default implementation uses round trip average measuring.
@@ -128,4 +149,25 @@ abstract class ConnectivityService : ServiceFacade() {
128149
}
129150
return false // assume is not slow on non mature connections
130151
}
131-
}
152+
153+
/**
154+
* Executes the given block when connectivity is available.
155+
* If connectivity is already available, executes immediately.
156+
* Otherwise, schedules execution for when connectivity is restored.
157+
*
158+
* @param block The code to execute when connectivity is available
159+
* @return A job that can be cancelled if needed
160+
*/
161+
fun runWhenConnected(block: suspend () -> Unit): Job {
162+
return serviceScope.launch {
163+
if (isConnected()) {
164+
launchIO { block() }
165+
} else {
166+
mutex.withLock {
167+
pendingConnectivityBlocks.add(block)
168+
log.d { "Added block to be run when connectivity restarts" }
169+
}
170+
}
171+
}
172+
}
173+
}

shared/domain/src/commonMain/kotlin/network/bisq/mobile/i18n/GeneratedResourceBundles_cs.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,6 +1550,7 @@ object GeneratedResourceBundles_cs {
15501550
"mobile.tradeState.info.buyer.phase2a.reasonForPaymentInfo" to "Use the trade ID {0} for the ''Reason for payment'' field",
15511551
"min" to "Min",
15521552
"bootstrap.connectedToTrustedNode" to "Connected to trusted node",
1553+
"bootstrap.noConnectivity" to "Žádné připojení: Čekání na připojení pro opakování",
15531554
"confirmation.areYouSure" to "Are you sure?",
15541555
"genericError.headline" to "An error occurred",
15551556
"mobile.reputation.learnMore" to "Learn more about the reputation system at the Bisq Wiki.",

shared/domain/src/commonMain/kotlin/network/bisq/mobile/i18n/GeneratedResourceBundles_de.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,7 @@ object GeneratedResourceBundles_de {
632632
"bisqEasy.offerbook.dropdownMenu.sortAndFilterMarkets.sortTitle" to "Sortieren nach:",
633633
"bisqEasy.tradeWizard.review.priceDescription.taker" to "Handelspreis",
634634
"bisqEasy.takeOffer.review.sendTakeOfferMessageFeedback.subTitle" to "Das Senden der Nachricht zum Akzeptieren des Angebots kann bis zu 2 Minuten dauern",
635-
"bisqEasy.walletGuide.receive.headline" to "Bitcoin in deiner Wallet empfangen",
635+
"bootstrap.noConnectivity" to "Keine Verbindung: Bitte überprüfen Sie Ihre Internetverbindung und starten Sie neu",
636636
"bisqEasy.tradeWizard.selectOffer.noMatchingOffers.headline" to "Keine passenden Angebote gefunden",
637637
"bisqEasy.walletGuide.tabs.headline" to "Wallet-Anleitung",
638638
"bisqEasy.offerbook.offerList.table.columns.fiatAmount" to "{0} Betrag",
@@ -1548,9 +1548,11 @@ object GeneratedResourceBundles_de {
15481548
"mobile.tradeState.info.buyer.phase2a.reasonForPaymentInfo" to "Use the trade ID {0} for the ''Reason for payment'' field",
15491549
"min" to "Min",
15501550
"bootstrap.connectedToTrustedNode" to "Connected to trusted node",
1551+
"bootstrap.noConnectivity" to "Keine Verbindung: Warte auf Verbindung für erneuten Versuch",
15511552
"confirmation.areYouSure" to "Are you sure?",
15521553
"genericError.headline" to "An error occurred",
15531554
"mobile.reputation.learnMore" to "Learn more about the reputation system at the Bisq Wiki.",
15541555
),
15551556
)
15561557
}
1558+

shared/domain/src/commonMain/kotlin/network/bisq/mobile/i18n/GeneratedResourceBundles_en.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,7 @@ object GeneratedResourceBundles_en {
15661566
"max" to "Max",
15671567
"error.exception" to "Exception",
15681568
"genericError.errorMessage" to "Error message:",
1569+
"bootstrap.noConnectivity" to "No Connectivity: Waiting to retry",
15691570
"mobile.openTrades.inMediation.banner" to "A mediator has joined the trade chat.\nPlease use the trade chat to get assistance from the mediator.",
15701571
"mobile.tradeState.info.buyer.phase2a.reasonForPaymentInfo" to "Use the trade ID {0} for the ''Reason for payment'' field",
15711572
"min" to "Min",

shared/domain/src/commonMain/kotlin/network/bisq/mobile/i18n/GeneratedResourceBundles_es.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ object GeneratedResourceBundles_es {
659659
"bisqEasy.tradeWizard.progress.amountAndPrice.selectOffer" to "Cantidad",
660660
"bisqEasy.offerDetails.makersTradeTerms" to "Términos comerciales del creador",
661661
"bisqEasy.openTrades.chat.attach.tooltip" to "Restaurar chat de nuevo en la ventana principal",
662-
"bisqEasy.tradeWizard.amount.seller.limitInfo" to "Tu cantidad máxima de venta es {0}.",
662+
"bootstrap.noConnectivity" to "Sin conectividad: Por favor, compruebe su conexión a internet y reinicie",
663663
"bisqEasy.privateChats.table.myUser" to "Mi perfil",
664664
"bisqEasy.tradeState.info.phase4.exportTrade" to "Exportar datos de la operación",
665665
"bisqEasy.tradeWizard.selectOffer.subHeadline" to "Se recomienda intercambiar con usuarios con alta reputación.",
@@ -1548,9 +1548,11 @@ object GeneratedResourceBundles_es {
15481548
"mobile.tradeState.info.buyer.phase2a.reasonForPaymentInfo" to "Use the trade ID {0} for the ''Reason for payment'' field",
15491549
"min" to "Min",
15501550
"bootstrap.connectedToTrustedNode" to "Connected to trusted node",
1551+
"bootstrap.noConnectivity" to "Sin conectividad: Esperando conexión para reintentar",
15511552
"confirmation.areYouSure" to "Are you sure?",
15521553
"genericError.headline" to "An error occurred",
15531554
"mobile.reputation.learnMore" to "Learn more about the reputation system at the Bisq Wiki.",
15541555
),
15551556
)
15561557
}
1558+

shared/domain/src/commonMain/kotlin/network/bisq/mobile/i18n/GeneratedResourceBundles_it.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,7 @@ object GeneratedResourceBundles_it {
15481548
"mobile.tradeState.info.buyer.phase2a.reasonForPaymentInfo" to "Use the trade ID {0} for the ''Reason for payment'' field",
15491549
"min" to "Min",
15501550
"bootstrap.connectedToTrustedNode" to "Connected to trusted node",
1551+
"bootstrap.noConnectivity" to "Nessuna connettività: In attesa di connessione per riprovare",
15511552
"confirmation.areYouSure" to "Are you sure?",
15521553
"genericError.headline" to "An error occurred",
15531554
"mobile.reputation.learnMore" to "Learn more about the reputation system at the Bisq Wiki.",

shared/domain/src/commonMain/kotlin/network/bisq/mobile/i18n/GeneratedResourceBundles_pcm.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,6 +1533,7 @@ object GeneratedResourceBundles_pcm {
15331533
"mobile.tradeState.info.buyer.phase2a.reasonForPaymentInfo" to "Use the trade ID {0} for the ''Reason for payment'' field",
15341534
"min" to "Min",
15351535
"bootstrap.connectedToTrustedNode" to "Connected to trusted node",
1536+
"bootstrap.noConnectivity" to "No Connectivity: Dey wait for connection to try again",
15361537
"confirmation.areYouSure" to "Are you sure?",
15371538
"genericError.headline" to "An error occurred",
15381539
"mobile.reputation.learnMore" to "Learn more about the reputation system at the Bisq Wiki.",

shared/domain/src/commonMain/kotlin/network/bisq/mobile/i18n/GeneratedResourceBundles_pt_BR.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,7 @@ object GeneratedResourceBundles_pt_BR {
15481548
"mobile.tradeState.info.buyer.phase2a.reasonForPaymentInfo" to "Use the trade ID {0} for the ''Reason for payment'' field",
15491549
"min" to "Min",
15501550
"bootstrap.connectedToTrustedNode" to "Connected to trusted node",
1551+
"bootstrap.noConnectivity" to "Sem conectividade: Aguardando conectividade para tentar novamente",
15511552
"confirmation.areYouSure" to "Are you sure?",
15521553
"genericError.headline" to "An error occurred",
15531554
"mobile.reputation.learnMore" to "Learn more about the reputation system at the Bisq Wiki.",

shared/domain/src/commonMain/kotlin/network/bisq/mobile/i18n/GeneratedResourceBundles_ru.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,7 @@ object GeneratedResourceBundles_ru {
15481548
"mobile.tradeState.info.buyer.phase2a.reasonForPaymentInfo" to "Use the trade ID {0} for the ''Reason for payment'' field",
15491549
"min" to "Min",
15501550
"bootstrap.connectedToTrustedNode" to "Connected to trusted node",
1551+
"bootstrap.noConnectivity" to "Нет соединения: Ожидание подключения для повторной попытки",
15511552
"confirmation.areYouSure" to "Are you sure?",
15521553
"genericError.headline" to "An error occurred",
15531554
"mobile.reputation.learnMore" to "Learn more about the reputation system at the Bisq Wiki.",

shared/domain/src/commonMain/resources/mobile/default.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ data.add=Add
5757
data.remove=Remove
5858
data.redacted=Data has been removed for privacy and security reasons
5959

60+
bootstrap.noConnectivity=No connectivity: Waiting to retry
61+
6062
offer.createOffer=Create offer
6163
offer.takeOffer.buy.button=Buy Bitcoin
6264
offer.takeOffer.sell.button=Sell Bitcoin

shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/MainPresenter.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ open class MainPresenter(
127127
super.onDestroying()
128128
}
129129

130+
fun isConnected(): Boolean {
131+
return connectivityService.isConnected()
132+
}
133+
130134
open fun reactivateServices() {
131135
log.d { "Reactivating services default: skip" }
132136
}

shared/presentation/src/commonMain/kotlin/network/bisq/mobile/presentation/ui/uicases/startup/SplashPresenter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@ open class SplashPresenter(
5050
private fun navigateToNextScreen() {
5151
log.d { "Navigating to next screen" }
5252
launchUI {
53+
// Check connectivity first
5354
if (!hasConnectivity()) {
55+
log.d { "No connectivity detected, navigating to trusted node setup" }
5456
navigateToTrustedNodeSetup()
57+
return@launchUI
5558
}
5659

5760
if (webSocketClientProvider?.get()?.isDemo() == true) {

0 commit comments

Comments
 (0)