Skip to content

Commit d98f55d

Browse files
Merge pull request #610 from bitcoinppl/fix-splash-stall
Fix cold-launch splash stall
2 parents 7fa46e0 + 58d3665 commit d98f55d

File tree

16 files changed

+2625
-304
lines changed

16 files changed

+2625
-304
lines changed

android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import androidx.compose.material3.AlertDialog
3737
import androidx.compose.material3.BottomSheetDefaults
3838
import androidx.compose.material3.CircularProgressIndicator
3939
import androidx.compose.material3.ExperimentalMaterial3Api
40+
import androidx.compose.material3.LinearProgressIndicator
4041
import androidx.compose.material3.FilledTonalButton
4142
import androidx.compose.material3.MaterialTheme
4243
import androidx.compose.material3.ModalBottomSheet
@@ -65,7 +66,11 @@ import androidx.compose.ui.res.painterResource
6566
import androidx.compose.ui.unit.dp
6667
import androidx.compose.ui.window.Dialog
6768
import androidx.fragment.app.FragmentActivity
69+
import androidx.lifecycle.lifecycleScope
70+
import kotlinx.coroutines.async
71+
import kotlinx.coroutines.coroutineScope
6872
import kotlinx.coroutines.delay
73+
import kotlinx.coroutines.launch
6974
import org.bitcoinppl.cove.flows.TapSignerFlow.TapSignerContainer
7075
import org.bitcoinppl.cove.navigation.CoveNavDisplay
7176
import org.bitcoinppl.cove.nfc.NfcScanSheet
@@ -75,7 +80,13 @@ import org.bitcoinppl.cove.ui.theme.CoveTheme
7580
import org.bitcoinppl.cove.views.LockView
7681
import org.bitcoinppl.cove.views.TermsAndConditionsSheet
7782
import org.bitcoinppl.cove_core.bootstrap
83+
import org.bitcoinppl.cove_core.activeMigration
84+
import org.bitcoinppl.cove_core.bootstrapProgress
85+
import org.bitcoinppl.cove.utils.isMigrationInProgress
86+
import org.bitcoinppl.cove_core.cancelBootstrap
7887
import org.bitcoinppl.cove_core.AfterPinAction
88+
import org.bitcoinppl.cove_core.AppInitException
89+
import org.bitcoinppl.cove_core.BootstrapStep
7990
import org.bitcoinppl.cove_core.AlertDisplayType
8091
import org.bitcoinppl.cove_core.AppAction
8192
import org.bitcoinppl.cove_core.AppAlertState
@@ -168,26 +179,83 @@ class MainActivity : FragmentActivity() {
168179
BootstrapErrorView(errorMessage = bootstrapError!!)
169180
} else {
170181
var showSpinner by remember { mutableStateOf(false) }
171-
SplashLoadingView(showSpinner = showSpinner)
182+
var splashStatus by remember { mutableStateOf<String?>(null) }
183+
var encryptionProgress by remember { mutableStateOf<Float?>(null) }
184+
185+
SplashLoadingView(
186+
showSpinner = showSpinner,
187+
statusMessage = splashStatus,
188+
progress = encryptionProgress,
189+
)
172190

173191
LaunchedEffect(Unit) {
174192
delay(SPINNER_DELAY_MS)
175193
showSpinner = true
176194
}
177195

178196
LaunchedEffect(Unit) {
179-
try {
180-
val warning = bootstrap()
197+
fun completeBootstrap(warning: String? = null) {
198+
splashStatus = null
199+
encryptionProgress = null
181200
(application as CoveApplication).onBootstrapComplete()
182201
val appInstance = AppManager.getInstance()
183-
appInstance.rust.initData()
184202
appInstance.asyncRuntimeReady = true
185203
isBootstrapped = true
186204
bootstrapped = true
187205
bdkMigrationWarning = warning
206+
207+
// non-blocking — initData preloads caches and prices but is not
208+
// required for core functionality, failures are logged but not surfaced to the user
209+
this@MainActivity.lifecycleScope.launch {
210+
appInstance.rust.initData()
211+
Log.d(TAG, "[STARTUP] initData completed")
212+
}
213+
}
214+
215+
val warning: String?
216+
217+
try {
218+
warning = runBootstrapWithWatchdog { status, progress ->
219+
splashStatus = status
220+
encryptionProgress = progress
221+
}
222+
} catch (e: BootstrapTimeoutException) {
223+
val step = bootstrapProgress()
224+
225+
if (step == BootstrapStep.COMPLETE) {
226+
// BDK migration retries every launch, so any lost warning will surface next time
227+
Log.w(TAG, "[STARTUP] bootstrap completed despite timeout — migration warning (if any) was lost and will retry on next launch")
228+
completeBootstrap()
229+
} else {
230+
Log.e(TAG, "[STARTUP] bootstrap timed out, last step: $step")
231+
bootstrapError =
232+
"App startup timed out. Please force-quit and try again.\n\nPlease contact feedback@covebitcoinwallet.com"
233+
}
234+
235+
return@LaunchedEffect
236+
} catch (e: kotlinx.coroutines.CancellationException) {
237+
throw e
188238
} catch (e: Exception) {
189-
bootstrapError = e.message ?: "Unknown error"
239+
val step = bootstrapProgress()
240+
if (step == BootstrapStep.COMPLETE) {
241+
Log.w(TAG, "[STARTUP] bootstrap completed despite error — treating as success", e)
242+
completeBootstrap()
243+
} else if (e is AppInitException.AlreadyCalled) {
244+
Log.e(TAG, "[STARTUP] bootstrap already called at step: $step", e)
245+
bootstrapError =
246+
"App initialization error. Please force-quit and restart."
247+
} else if (e is AppInitException.Cancelled) {
248+
Log.e(TAG, "[STARTUP] bootstrap cancelled at step: $step", e)
249+
bootstrapError =
250+
"App startup timed out. Please force-quit and try again.\n\nPlease contact feedback@covebitcoinwallet.com"
251+
} else {
252+
Log.e(TAG, "[STARTUP] bootstrap failed at step: $step", e)
253+
bootstrapError = e.message ?: "Unknown error"
254+
}
255+
return@LaunchedEffect
190256
}
257+
258+
completeBootstrap(warning)
191259
}
192260
}
193261
return@setContent
@@ -302,6 +370,53 @@ class MainActivity : FragmentActivity() {
302370
privacyCoverView = container
303371
}
304372

373+
private suspend fun runBootstrapWithWatchdog(
374+
onMigrationProgress: (status: String?, progress: Float?) -> Unit,
375+
): String? = coroutineScope {
376+
val bootstrapDeferred = async { bootstrap() }
377+
launch { watchBootstrap(bootstrapDeferred, onMigrationProgress) }
378+
bootstrapDeferred.await()
379+
}
380+
381+
private suspend fun watchBootstrap(
382+
bootstrapDeferred: kotlinx.coroutines.Deferred<*>,
383+
onMigrationProgress: (status: String?, progress: Float?) -> Unit,
384+
) {
385+
val startTime = System.currentTimeMillis()
386+
var migrationDetected = false
387+
var progressCleared = true
388+
389+
while (bootstrapDeferred.isActive) {
390+
delay(66)
391+
392+
val step = bootstrapProgress()
393+
if (!migrationDetected && step.isMigrationInProgress) {
394+
migrationDetected = true
395+
}
396+
397+
val progress = activeMigration()?.progress()
398+
if (progress != null && progress.total > 0u) {
399+
migrationDetected = true
400+
progressCleared = false
401+
onMigrationProgress("Encrypting data...", progress.current.toFloat() / progress.total.toFloat())
402+
} else if (!progressCleared) {
403+
progressCleared = true
404+
onMigrationProgress(null, null)
405+
}
406+
407+
val elapsed = System.currentTimeMillis() - startTime
408+
// longer timeout to accommodate low-end Android hardware
409+
val timeoutMs = if (migrationDetected) 60_000L else 20_000L
410+
if (elapsed >= timeoutMs && bootstrapDeferred.isActive) {
411+
Log.w(TAG, "[STARTUP] watchdog firing after ${elapsed}ms (timeout=${timeoutMs}ms, migration=$migrationDetected)")
412+
cancelBootstrap()
413+
throw BootstrapTimeoutException()
414+
}
415+
}
416+
}
417+
418+
private class BootstrapTimeoutException : Exception("bootstrap timed out")
419+
305420
companion object {
306421
/** Delay before showing the loading spinner, in milliseconds.
307422
* Prevents a distracting spinner flash when bootstrap completes quickly */
@@ -849,7 +964,11 @@ private fun BootstrapErrorView(errorMessage: String) {
849964
}
850965

851966
@Composable
852-
private fun SplashLoadingView(showSpinner: Boolean) {
967+
private fun SplashLoadingView(
968+
showSpinner: Boolean,
969+
statusMessage: String? = null,
970+
progress: Float? = null,
971+
) {
853972
Box(
854973
modifier = Modifier.fillMaxSize().background(Color.Black),
855974
contentAlignment = Alignment.Center,
@@ -864,6 +983,25 @@ private fun SplashLoadingView(showSpinner: Boolean) {
864983
Spacer(modifier = Modifier.height(24.dp))
865984
CircularProgressIndicator(color = Color.White)
866985
}
986+
987+
if (statusMessage != null) {
988+
Spacer(modifier = Modifier.height(12.dp))
989+
Text(
990+
statusMessage,
991+
style = MaterialTheme.typography.bodyMedium,
992+
color = Color.White.copy(alpha = 0.7f),
993+
)
994+
}
995+
996+
if (progress != null) {
997+
Spacer(modifier = Modifier.height(12.dp))
998+
LinearProgressIndicator(
999+
progress = { progress },
1000+
modifier = Modifier.fillMaxWidth(0.6f),
1001+
color = Color.White,
1002+
trackColor = Color.White.copy(alpha = 0.2f),
1003+
)
1004+
}
8671005
}
8681006
}
8691007
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.bitcoinppl.cove.utils
2+
3+
import org.bitcoinppl.cove_core.BootstrapStep
4+
import org.bitcoinppl.cove_core.bootstrapStepIsMigrationInProgress
5+
6+
val BootstrapStep.isMigrationInProgress: Boolean
7+
get() = bootstrapStepIsMigrationInProgress(this)

0 commit comments

Comments
 (0)