Files
GajuMobile-Android/app/src/main/java/swiss/qpq/gajumobile/MainActivity.kt
T
2026-08-22 18:45:28 +09:00

429 lines
17 KiB
Kotlin

package swiss.qpq.gajumobile
import android.os.Bundle
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ProcessLifecycleOwner
import swiss.qpq.gajumobile.data.AppStateManager
import swiss.qpq.gajumobile.data.WalletRepository
import swiss.qpq.gajumobile.data.models.Account
import swiss.qpq.gajumobile.data.models.Wallet
import swiss.qpq.gajumobile.security.AppLifecycleObserver
import swiss.qpq.gajumobile.security.BiometricHelper
import swiss.qpq.gajumobile.security.SecurityManager
import swiss.qpq.gajumobile.security.KeyManager
import swiss.qpq.gajumobile.security.AccountAirlock
import swiss.qpq.gajumobile.ui.screens.AccountNamingScreen
import swiss.qpq.gajumobile.ui.screens.CreateWalletScreen
import swiss.qpq.gajumobile.ui.screens.DashboardScreen
import swiss.qpq.gajumobile.ui.screens.DeleteConfirmationScreen
import swiss.qpq.gajumobile.ui.screens.EnvironmentInfoScreen
import swiss.qpq.gajumobile.ui.screens.ExplorerScreen
import swiss.qpq.gajumobile.ui.screens.LockScreen
import swiss.qpq.gajumobile.ui.screens.ManageAccountScreen
import swiss.qpq.gajumobile.ui.screens.ManageWalletScreen
import swiss.qpq.gajumobile.ui.screens.MnemonicRecoveryScreen
import swiss.qpq.gajumobile.ui.screens.MnemonicVerifyScreen
import swiss.qpq.gajumobile.ui.screens.QRReceiveScreen
import swiss.qpq.gajumobile.ui.screens.QRScannerScreen
import swiss.qpq.gajumobile.ui.screens.SettingsScreen
import swiss.qpq.gajumobile.ui.screens.SetupChoiceScreen
import swiss.qpq.gajumobile.ui.screens.ViewMnemonicScreen
import swiss.qpq.gajumobile.ui.theme.MyApplicationTheme
import swiss.qpq.gajumaru.core.formatting.GajuFormat
import java.util.UUID
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
window.isNavigationBarContrastEnforced = false
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver(this))
setContent {
val context = LocalContext.current
val isLocked by SecurityManager.isLocked.collectAsState()
var lockErrorMessage by remember { mutableStateOf<String?>(null) }
val walletRepo = remember { WalletRepository(context) }
val appStateManager = remember { AppStateManager(context) }
MyApplicationTheme {
if (isLocked) {
LockScreen(errorMessage = lockErrorMessage) {
BiometricHelper.showBiometricPrompt(
activity = this,
onSuccess = {
lockErrorMessage = null
SecurityManager.unlock()
},
onError = { err ->
lockErrorMessage = err
}
)
}
} else {
val routerError: (String) -> Unit = { message ->
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
GajuRouter(walletRepo, appStateManager, routerError)
}
}
}
}
}
enum class Screen {
CREATE_WALLET,
SETUP_CHOICE,
MNEMONIC_RECOVERY,
ACCOUNT_NAMING,
DASHBOARD,
QR_SCANNER,
QR_RECEIVE,
SETTINGS,
MANAGE_WALLET,
MANAGE_ACCOUNT,
DELETE_CONFIRM,
EXPLORER,
VIEW_MNEMONIC,
VERIFY_MNEMONIC,
ENVIRONMENT_INFO
}
@Composable
fun GajuRouter(
walletRepo: WalletRepository,
appStateManager: AppStateManager,
onError: (String) -> Unit
) {
val context = LocalContext.current
val appState by appStateManager.state.collectAsState()
val initialScreen = remember {
if (walletRepo.listWallets().isEmpty()) Screen.CREATE_WALLET else Screen.DASHBOARD
}
val screenStack = remember { mutableStateListOf(initialScreen) }
val currentScreen = screenStack.last()
fun navigateTo(screen: Screen) {
screenStack.add(screen)
}
fun navigateBack() {
if (screenStack.size > 1) {
screenStack.removeAt(screenStack.size - 1)
}
}
BackHandler(enabled = screenStack.size > 1) {
navigateBack()
}
var pendingWalletName by rememberSaveable { mutableStateOf("") }
var pendingAccount by remember { mutableStateOf<Account?>(null) }
var setupRecovered by rememberSaveable { mutableStateOf(false) }
var selectedWalletId by rememberSaveable { mutableStateOf("") }
var selectedAccountId by rememberSaveable { mutableStateOf("") }
var deleteType by rememberSaveable { mutableStateOf("") }
var deleteName by rememberSaveable { mutableStateOf("") }
var viewMnemonicAccount by remember { mutableStateOf<Account?>(null) }
var viewMnemonicPhrase by remember { mutableStateOf<Array<ByteArray>?>(null) }
var verifyMnemonicAccount by remember { mutableStateOf<Account?>(null) }
var verifyMnemonicPhrase by remember { mutableStateOf<Array<ByteArray>?>(null) }
when (currentScreen) {
Screen.CREATE_WALLET -> {
CreateWalletScreen(
onWalletCreated = { name ->
pendingWalletName = name
selectedWalletId = ""
navigateTo(Screen.SETUP_CHOICE)
},
onBack = if (screenStack.size > 1) { { navigateBack() } } else null
)
}
Screen.SETUP_CHOICE -> {
SetupChoiceScreen(
onCreate = {
try {
val masterKey = KeyManager.getMasterKey(context)
val phrase = AccountAirlock.generateMnemonic()
val account = AccountAirlock.createAccount("New Account", phrase, masterKey)
AccountAirlock.wipePhrase(phrase)
pendingAccount = account
setupRecovered = false
navigateTo(Screen.ACCOUNT_NAMING)
} catch (e: Exception) {
onError("Failed to generate account: ${e.message}")
}
},
onRecover = {
setupRecovered = true
navigateTo(Screen.MNEMONIC_RECOVERY)
},
onBack = { navigateBack() }
)
}
Screen.MNEMONIC_RECOVERY -> {
MnemonicRecoveryScreen(
onMnemonicConfirmed = { phrase ->
try {
val masterKey = KeyManager.getMasterKey(context)
val account = AccountAirlock.createAccount("Recovered Account", phrase, masterKey)
AccountAirlock.wipePhrase(phrase)
pendingAccount = account
navigateTo(Screen.ACCOUNT_NAMING)
} catch (e: Exception) {
onError("Failed to recover account: ${e.message}")
}
},
onBack = { navigateBack() }
)
}
Screen.ACCOUNT_NAMING -> {
val account = pendingAccount ?: return
val akId = account.gajuId
AccountNamingScreen(
publicKey = akId,
onAccountNamed = { name ->
val finalAccount = account.copy(label = name)
if (selectedWalletId.isNotEmpty()) {
val wallet = walletRepo.loadWallet(selectedWalletId)
if (wallet != null) {
walletRepo.saveWallet(wallet.copy(accounts = wallet.accounts + finalAccount))
}
} else {
val wallet = Wallet(
id = UUID.randomUUID().toString(),
name = pendingWalletName.ifBlank { "My Wallet" },
accounts = listOf(finalAccount)
)
walletRepo.saveWallet(wallet)
}
if (!setupRecovered) {
appStateManager.markUnverified(finalAccount.id)
}
selectedWalletId = ""
appStateManager.notifyWalletsChanged()
screenStack.clear()
screenStack.add(Screen.DASHBOARD)
},
onBack = { navigateBack() }
)
}
Screen.VIEW_MNEMONIC -> {
val acc = viewMnemonicAccount ?: return
val phrase = viewMnemonicPhrase ?: return
ViewMnemonicScreen(label = acc.label, phrase = phrase) {
AccountAirlock.wipePhrase(phrase)
viewMnemonicAccount = null
viewMnemonicPhrase = null
navigateBack()
}
}
Screen.VERIFY_MNEMONIC -> {
val acc = verifyMnemonicAccount ?: return
val phrase = verifyMnemonicPhrase ?: return
MnemonicVerifyScreen(
label = acc.label,
expectedPhrase = phrase,
onVerified = {
appStateManager.clearVerified(acc.id)
AccountAirlock.wipePhrase(phrase)
verifyMnemonicAccount = null
verifyMnemonicPhrase = null
navigateBack()
},
onBack = {
AccountAirlock.wipePhrase(phrase)
verifyMnemonicAccount = null
verifyMnemonicPhrase = null
navigateBack()
}
)
}
Screen.DELETE_CONFIRM -> {
DeleteConfirmationScreen(
type = deleteType,
name = deleteName,
onConfirm = {
if (deleteType == "WALLET") {
walletRepo.deleteWallet(selectedWalletId)
} else {
walletRepo.deleteAccount(selectedWalletId, selectedAccountId)
}
appStateManager.notifyWalletsChanged()
screenStack.removeAt(screenStack.size - 1)
screenStack.removeAt(screenStack.size - 1)
},
onCancel = { navigateBack() }
)
}
Screen.DASHBOARD -> {
val wallets = remember(appState.walletsVersion) {
walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) }
}
DashboardScreen(
wallets = wallets,
balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat),
balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit),
onSend = { navigateTo(Screen.QR_SCANNER) },
onReceive = { navigateTo(Screen.QR_RECEIVE) },
onAddWallet = {
selectedWalletId = ""
navigateTo(Screen.CREATE_WALLET)
},
onAddAccount = { walletId ->
selectedWalletId = walletId
navigateTo(Screen.SETUP_CHOICE)
},
onAccountSettings = { walletId, accountId ->
selectedWalletId = walletId
selectedAccountId = accountId
navigateTo(Screen.MANAGE_ACCOUNT)
},
onNavigate = { dest ->
val screen = when (dest) {
"settings" -> Screen.SETTINGS
"transactions" -> Screen.EXPLORER
"environment_info" -> Screen.ENVIRONMENT_INFO
else -> null
}
if (screen != null) navigateTo(screen)
},
)
}
Screen.MANAGE_WALLET -> {
val wallet = remember(selectedWalletId, appState.walletsVersion) {
walletRepo.loadWallet(selectedWalletId)
} ?: return
ManageWalletScreen(
walletName = wallet.name,
onRename = { newName ->
walletRepo.saveWallet(wallet.copy(name = newName))
appStateManager.notifyWalletsChanged()
},
onDelete = {
deleteType = "WALLET"
deleteName = wallet.name
navigateTo(Screen.DELETE_CONFIRM)
}
)
}
Screen.MANAGE_ACCOUNT -> {
val wallet = remember(selectedWalletId, appState.walletsVersion) {
walletRepo.loadWallet(selectedWalletId)
} ?: return
val account = wallet.accounts.find { it.id == selectedAccountId } ?: return
val isUnverified = appState.unverifiedAccountIds.contains(account.id)
ManageAccountScreen(
account = account,
walletName = wallet.name,
balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat),
balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit),
isUnverified = isUnverified,
onViewMnemonic = {
try {
val masterKey = KeyManager.getMasterKey(context)
val phrase = AccountAirlock.deriveMnemonic(account.privateKeyEnvelope, masterKey)
viewMnemonicAccount = account
viewMnemonicPhrase = phrase
navigateTo(Screen.VIEW_MNEMONIC)
} catch (e: Exception) {
onError("Authentication failed: ${e.message}")
}
},
onVerifyMnemonic = {
try {
val masterKey = KeyManager.getMasterKey(context)
val phrase = AccountAirlock.deriveMnemonic(account.privateKeyEnvelope, masterKey)
verifyMnemonicAccount = account
verifyMnemonicPhrase = phrase
navigateTo(Screen.VERIFY_MNEMONIC)
} catch (e: Exception) {
onError("Authentication failed: ${e.message}")
}
},
onRename = { newName ->
val updatedAccounts = wallet.accounts.map {
if (it.id == account.id) it.copy(label = newName) else it
}
walletRepo.saveWallet(wallet.copy(accounts = updatedAccounts))
appStateManager.notifyWalletsChanged()
},
onDelete = {
deleteType = "ACCOUNT"
deleteName = account.label
navigateTo(Screen.DELETE_CONFIRM)
}
)
}
Screen.SETTINGS -> {
SettingsScreen(
balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat),
onFormatChange = { type ->
appStateManager.setBalanceFormat(type.name)
},
onNavigate = { dest ->
val screen = when (dest) {
"dashboard" -> Screen.DASHBOARD
"transactions" -> Screen.EXPLORER
"environment_info" -> Screen.ENVIRONMENT_INFO
else -> null
}
if (screen != null) {
screenStack.clear()
screenStack.add(screen)
}
}
)
}
Screen.ENVIRONMENT_INFO -> {
EnvironmentInfoScreen(onBack = { navigateBack() })
}
Screen.QR_SCANNER -> QRScannerScreen(onGenerate = { navigateTo(Screen.QR_RECEIVE) }, onUpload = {})
Screen.QR_RECEIVE -> QRReceiveScreen(onDone = { navigateBack() })
Screen.EXPLORER -> ExplorerScreen(onNavigate = { dest ->
val screen = when (dest) {
"dashboard" -> Screen.DASHBOARD
"settings" -> Screen.SETTINGS
else -> null
}
if (screen != null) {
screenStack.clear()
screenStack.add(screen)
}
})
}
}