Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 144 additions & 6 deletions android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
Expand Down Expand Up @@ -65,7 +66,11 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.bitcoinppl.cove.flows.TapSignerFlow.TapSignerContainer
import org.bitcoinppl.cove.navigation.CoveNavDisplay
import org.bitcoinppl.cove.nfc.NfcScanSheet
Expand All @@ -75,7 +80,13 @@ import org.bitcoinppl.cove.ui.theme.CoveTheme
import org.bitcoinppl.cove.views.LockView
import org.bitcoinppl.cove.views.TermsAndConditionsSheet
import org.bitcoinppl.cove_core.bootstrap
import org.bitcoinppl.cove_core.activeMigration
import org.bitcoinppl.cove_core.bootstrapProgress
import org.bitcoinppl.cove.utils.isMigrationInProgress
import org.bitcoinppl.cove_core.cancelBootstrap
import org.bitcoinppl.cove_core.AfterPinAction
import org.bitcoinppl.cove_core.AppInitException
import org.bitcoinppl.cove_core.BootstrapStep
import org.bitcoinppl.cove_core.AlertDisplayType
import org.bitcoinppl.cove_core.AppAction
import org.bitcoinppl.cove_core.AppAlertState
Expand Down Expand Up @@ -168,26 +179,83 @@ class MainActivity : FragmentActivity() {
BootstrapErrorView(errorMessage = bootstrapError!!)
} else {
var showSpinner by remember { mutableStateOf(false) }
SplashLoadingView(showSpinner = showSpinner)
var splashStatus by remember { mutableStateOf<String?>(null) }
var encryptionProgress by remember { mutableStateOf<Float?>(null) }

SplashLoadingView(
showSpinner = showSpinner,
statusMessage = splashStatus,
progress = encryptionProgress,
)

LaunchedEffect(Unit) {
delay(SPINNER_DELAY_MS)
showSpinner = true
}

LaunchedEffect(Unit) {
try {
val warning = bootstrap()
fun completeBootstrap(warning: String? = null) {
splashStatus = null
encryptionProgress = null
(application as CoveApplication).onBootstrapComplete()
val appInstance = AppManager.getInstance()
appInstance.rust.initData()
appInstance.asyncRuntimeReady = true
isBootstrapped = true
bootstrapped = true
bdkMigrationWarning = warning

// non-blocking — initData preloads caches and prices but is not
// required for core functionality, failures are logged but not surfaced to the user
this@MainActivity.lifecycleScope.launch {
appInstance.rust.initData()
Log.d(TAG, "[STARTUP] initData completed")
}
}

val warning: String?

try {
warning = runBootstrapWithWatchdog { status, progress ->
splashStatus = status
encryptionProgress = progress
}
} catch (e: BootstrapTimeoutException) {
val step = bootstrapProgress()

if (step == BootstrapStep.COMPLETE) {
// BDK migration retries every launch, so any lost warning will surface next time
Log.w(TAG, "[STARTUP] bootstrap completed despite timeout — migration warning (if any) was lost and will retry on next launch")
completeBootstrap()
} else {
Log.e(TAG, "[STARTUP] bootstrap timed out, last step: $step")
bootstrapError =
"App startup timed out. Please force-quit and try again.\n\nPlease contact feedback@covebitcoinwallet.com"
}

return@LaunchedEffect
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
bootstrapError = e.message ?: "Unknown error"
val step = bootstrapProgress()
if (step == BootstrapStep.COMPLETE) {
Log.w(TAG, "[STARTUP] bootstrap completed despite error — treating as success", e)
completeBootstrap()
} else if (e is AppInitException.AlreadyCalled) {
Log.e(TAG, "[STARTUP] bootstrap already called at step: $step", e)
bootstrapError =
"App initialization error. Please force-quit and restart."
} else if (e is AppInitException.Cancelled) {
Log.e(TAG, "[STARTUP] bootstrap cancelled at step: $step", e)
bootstrapError =
"App startup timed out. Please force-quit and try again.\n\nPlease contact feedback@covebitcoinwallet.com"
} else {
Log.e(TAG, "[STARTUP] bootstrap failed at step: $step", e)
bootstrapError = e.message ?: "Unknown error"
}
return@LaunchedEffect
}

completeBootstrap(warning)
}
}
return@setContent
Expand Down Expand Up @@ -302,6 +370,53 @@ class MainActivity : FragmentActivity() {
privacyCoverView = container
}

private suspend fun runBootstrapWithWatchdog(
onMigrationProgress: (status: String?, progress: Float?) -> Unit,
): String? = coroutineScope {
val bootstrapDeferred = async { bootstrap() }
launch { watchBootstrap(bootstrapDeferred, onMigrationProgress) }
bootstrapDeferred.await()
}

private suspend fun watchBootstrap(
bootstrapDeferred: kotlinx.coroutines.Deferred<*>,
onMigrationProgress: (status: String?, progress: Float?) -> Unit,
) {
val startTime = System.currentTimeMillis()
var migrationDetected = false
var progressCleared = true

while (bootstrapDeferred.isActive) {
delay(66)

val step = bootstrapProgress()
if (!migrationDetected && step.isMigrationInProgress) {
migrationDetected = true
}

val progress = activeMigration()?.progress()
if (progress != null && progress.total > 0u) {
migrationDetected = true
progressCleared = false
onMigrationProgress("Encrypting data...", progress.current.toFloat() / progress.total.toFloat())
} else if (!progressCleared) {
Comment on lines +397 to +402
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Migration binding: class + close semantics =="
rg -n -C2 --type=kt '\bclass\s+Migration\b|\bAutoCloseable\b|\bfun\s+close\s*\(' android/app/src/main/java/org/bitcoinppl/cove_core

echo "== activeMigration signature =="
rg -n -C3 --type=kt '\bfun\s+activeMigration\s*\(' android/app/src/main/java/org/bitcoinppl/cove_core

echo "== activeMigration usage and cleanup patterns =="
rg -n -C3 --type=kt '\bactiveMigration\s*\(' android/app/src/main/java/org/bitcoinppl/cove
rg -n -C3 --type=kt '\b(activeMigration\s*\(\)\s*\.use|\.close\s*\()' android/app/src/main/java/org/bitcoinppl/cove

Repository: bitcoinppl/cove

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Directory structure =="
ls -la android/app/src/main/java/org/bitcoinppl/cove_core/ 2>/dev/null | head -20
ls -la android/app/src/main/java/org/bitcoinppl/cove/ 2>/dev/null | head -20

echo "== Find kotlin files =="
fd --type f '\.kt$' android/app/src/main/java/org/bitcoinppl/cove

echo "== Search for Migration class =="
rg -n 'class\s+Migration' android/app/src/main/java/org/bitcoinppl/

echo "== Search for activeMigration function =="
rg -n 'fun\s+activeMigration' android/app/src/main/java/org/bitcoinppl/

echo "== Search for AutoCloseable in bindings =="
rg -n 'AutoCloseable' android/app/src/main/java/org/bitcoinppl/

Repository: bitcoinppl/cove

Length of output: 13400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for activeMigration definition (broader) =="
rg -n 'activeMigration' android/app/src/main/java/org/bitcoinppl/ -A 2

echo "== Check MainActivity.kt watchdog loop context around 397-402 =="
sed -n '380,420p' android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt

echo "== Check for .progress() method on Migration =="
rg -n 'fun\s+progress' android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt | head -20

echo "== Check for existing .use patterns with activeMigration =="
rg -n -C2 'activeMigration.*\.use' android/app/src/main/java/org/bitcoinppl/cove

Repository: bitcoinppl/cove

Length of output: 2894


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Migration class definition and close method =="
sed -n '13697,13750p' android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt

echo "== Search for progress() method on Migration =="
rg -n 'fun progress' android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt | head -10

echo "== Check all activeMigration() usages in cove (not cove_core) =="
rg -n 'activeMigration' android/app/src/main/java/org/bitcoinppl/cove --type-add 'kt:*.kt'

echo "== Check for .use or .close patterns with Migration objects =="
rg -n '\.use.*Migration\|Migration.*\.close' android/app/src/main/java/org/bitcoinppl/cove --type-add 'kt:*.kt'

echo "== Search Migration class close method =="
sed -n '13697,13800p' android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt | grep -n 'close\|dispose'

Repository: bitcoinppl/cove

Length of output: 1974


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All usages of activeMigration in MainActivity =="
rg -n 'activeMigration' android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt

echo "== Check progress() return type and implementation =="
grep -n 'fun progress\|data class MigrationProgress' android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt | head -5

echo "== Get more context on progress() method =="
sed -n '13750,13850p' android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt | head -50

echo "== Check if MigrationProgress is AutoCloseable =="
rg -n 'data class MigrationProgress' android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt -A 10

Repository: bitcoinppl/cove

Length of output: 2370


Close the Migration object returned by activeMigration() in the watchdog loop.

Line 397 fetches activeMigration() on every tick and it returns an AutoCloseable object. Not closing it can leak FFI handles over repeated loop iterations. Use .use { ... } to ensure cleanup.

Proposed fix
-            val progress = activeMigration()?.progress()
+            val progress = activeMigration()?.use { it.progress() }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/MainActivity.kt` around lines
397 - 402, The watchdog loop currently calls activeMigration() (returns an
AutoCloseable Migration) and doesn't close it, leaking FFI handles; wrap the
activeMigration() call in a .use { migration -> ... } block (or check for null
then call .use) and move the progress() inspection and the existing logic
(setting migrationDetected/progressCleared and calling onMigrationProgress)
inside that block so the Migration is always closed after use; ensure you
preserve the else branch that checks !progressCleared outside/after the use
block as before.

progressCleared = true
onMigrationProgress(null, null)
}

val elapsed = System.currentTimeMillis() - startTime
// longer timeout to accommodate low-end Android hardware
val timeoutMs = if (migrationDetected) 60_000L else 20_000L
if (elapsed >= timeoutMs && bootstrapDeferred.isActive) {
Log.w(TAG, "[STARTUP] watchdog firing after ${elapsed}ms (timeout=${timeoutMs}ms, migration=$migrationDetected)")
cancelBootstrap()
throw BootstrapTimeoutException()
}
}
}

private class BootstrapTimeoutException : Exception("bootstrap timed out")

companion object {
/** Delay before showing the loading spinner, in milliseconds.
* Prevents a distracting spinner flash when bootstrap completes quickly */
Expand Down Expand Up @@ -849,7 +964,11 @@ private fun BootstrapErrorView(errorMessage: String) {
}

@Composable
private fun SplashLoadingView(showSpinner: Boolean) {
private fun SplashLoadingView(
showSpinner: Boolean,
statusMessage: String? = null,
progress: Float? = null,
) {
Box(
modifier = Modifier.fillMaxSize().background(Color.Black),
contentAlignment = Alignment.Center,
Expand All @@ -864,6 +983,25 @@ private fun SplashLoadingView(showSpinner: Boolean) {
Spacer(modifier = Modifier.height(24.dp))
CircularProgressIndicator(color = Color.White)
}

if (statusMessage != null) {
Spacer(modifier = Modifier.height(12.dp))
Text(
statusMessage,
style = MaterialTheme.typography.bodyMedium,
color = Color.White.copy(alpha = 0.7f),
)
}

if (progress != null) {
Spacer(modifier = Modifier.height(12.dp))
LinearProgressIndicator(
progress = { progress },
modifier = Modifier.fillMaxWidth(0.6f),
color = Color.White,
trackColor = Color.White.copy(alpha = 0.2f),
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.bitcoinppl.cove.utils

import org.bitcoinppl.cove_core.BootstrapStep
import org.bitcoinppl.cove_core.bootstrapStepIsMigrationInProgress

val BootstrapStep.isMigrationInProgress: Boolean
get() = bootstrapStepIsMigrationInProgress(this)
Loading
Loading