feat(android): 添加导出/导入备份功能的详细调试日志

添加日志位置:
- TssRepository: exportShareBackup 和 importShareBackup 函数
- MainViewModel: exportShareBackup 和 importShareBackup 函数
- MainActivity: 文件选择器回调、LaunchedEffect、导出/导入触发点

日志标签:
- [EXPORT] / [IMPORT]: Repository 和 ViewModel 层
- [EXPORT-FILE] / [IMPORT-FILE]: 文件选择器回调
- [EXPORT-EFFECT] / [IMPORT-EFFECT]: LaunchedEffect
- [EXPORT-TRIGGER] / [IMPORT-TRIGGER]: 用户操作触发点

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-26 05:39:56 -08:00
parent c002640911
commit 7b3d28c957
3 changed files with 134 additions and 9 deletions

View File

@ -123,57 +123,93 @@ fun TssPartyApp(
val createDocumentLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(ShareBackup.MIME_TYPE)
) { uri: Uri? ->
android.util.Log.d("MainActivity", "[EXPORT-FILE] ========== createDocumentLauncher callback ==========")
android.util.Log.d("MainActivity", "[EXPORT-FILE] uri: $uri")
android.util.Log.d("MainActivity", "[EXPORT-FILE] pendingExportJson isNull: ${pendingExportJson == null}")
android.util.Log.d("MainActivity", "[EXPORT-FILE] pendingExportJson length: ${pendingExportJson?.length ?: 0}")
uri?.let { targetUri ->
pendingExportJson?.let { json ->
try {
android.util.Log.d("MainActivity", "[EXPORT-FILE] Opening output stream to: $targetUri")
context.contentResolver.openOutputStream(targetUri)?.use { outputStream ->
android.util.Log.d("MainActivity", "[EXPORT-FILE] Writing ${json.length} bytes...")
outputStream.write(json.toByteArray(Charsets.UTF_8))
android.util.Log.d("MainActivity", "[EXPORT-FILE] Write completed")
}
android.util.Log.d("MainActivity", "[EXPORT-FILE] File saved successfully!")
Toast.makeText(context, "备份文件已保存", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
android.util.Log.e("MainActivity", "[EXPORT-FILE] Failed to save file: ${e.message}", e)
Toast.makeText(context, "保存失败: ${e.message}", Toast.LENGTH_LONG).show()
}
android.util.Log.d("MainActivity", "[EXPORT-FILE] Clearing pendingExportJson and pendingExportAddress")
pendingExportJson = null
pendingExportAddress = null
} ?: run {
android.util.Log.w("MainActivity", "[EXPORT-FILE] pendingExportJson is null, nothing to write!")
}
} ?: run {
android.util.Log.w("MainActivity", "[EXPORT-FILE] User cancelled file picker (uri is null)")
}
android.util.Log.d("MainActivity", "[EXPORT-FILE] ========== callback finished ==========")
}
// File picker for importing backup
val openDocumentLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
android.util.Log.d("MainActivity", "[IMPORT-FILE] ========== openDocumentLauncher callback ==========")
android.util.Log.d("MainActivity", "[IMPORT-FILE] uri: $uri")
uri?.let { sourceUri ->
try {
android.util.Log.d("MainActivity", "[IMPORT-FILE] Opening input stream from: $sourceUri")
context.contentResolver.openInputStream(sourceUri)?.use { inputStream ->
val json = inputStream.bufferedReader().readText()
android.util.Log.d("MainActivity", "[IMPORT-FILE] Read ${json.length} bytes")
android.util.Log.d("MainActivity", "[IMPORT-FILE] JSON preview: ${json.take(100)}...")
android.util.Log.d("MainActivity", "[IMPORT-FILE] Calling viewModel.importShareBackup...")
viewModel.importShareBackup(json)
android.util.Log.d("MainActivity", "[IMPORT-FILE] viewModel.importShareBackup called")
}
} catch (e: Exception) {
android.util.Log.e("MainActivity", "[IMPORT-FILE] Failed to read file: ${e.message}", e)
Toast.makeText(context, "读取文件失败: ${e.message}", Toast.LENGTH_LONG).show()
}
} ?: run {
android.util.Log.w("MainActivity", "[IMPORT-FILE] User cancelled file picker (uri is null)")
}
android.util.Log.d("MainActivity", "[IMPORT-FILE] ========== callback finished ==========")
}
// Handle export result - trigger file save dialog
LaunchedEffect(pendingExportJson) {
android.util.Log.d("MainActivity", "[EXPORT-EFFECT] LaunchedEffect(pendingExportJson) triggered")
android.util.Log.d("MainActivity", "[EXPORT-EFFECT] pendingExportJson isNull: ${pendingExportJson == null}")
android.util.Log.d("MainActivity", "[EXPORT-EFFECT] pendingExportJson length: ${pendingExportJson?.length ?: 0}")
pendingExportJson?.let { json ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val addressSuffix = pendingExportAddress?.take(8) ?: "wallet"
val fileName = "tss_backup_${addressSuffix}_$timestamp.${ShareBackup.FILE_EXTENSION}"
android.util.Log.d("MainActivity", "[EXPORT-EFFECT] Launching file picker with filename: $fileName")
createDocumentLauncher.launch(fileName)
android.util.Log.d("MainActivity", "[EXPORT-EFFECT] File picker launched")
}
}
// Handle import result - show toast
LaunchedEffect(importResult) {
android.util.Log.d("MainActivity", "[IMPORT-EFFECT] LaunchedEffect(importResult) triggered")
android.util.Log.d("MainActivity", "[IMPORT-EFFECT] importResult: $importResult")
importResult?.let { result ->
android.util.Log.d("MainActivity", "[IMPORT-EFFECT] isSuccess: ${result.isSuccess}, error: ${result.error}, message: ${result.message}")
when {
result.isSuccess -> {
android.util.Log.d("MainActivity", "[IMPORT-EFFECT] Showing success toast")
Toast.makeText(context, result.message ?: "导入成功", Toast.LENGTH_SHORT).show()
viewModel.clearExportImportResult()
}
result.error != null -> {
android.util.Log.d("MainActivity", "[IMPORT-EFFECT] Showing error toast: ${result.error}")
Toast.makeText(context, result.error, Toast.LENGTH_LONG).show()
viewModel.clearExportImportResult()
}
@ -184,6 +220,7 @@ fun TssPartyApp(
// Track if startup is complete
// Use rememberSaveable to persist across configuration changes (e.g., file picker activity)
var startupComplete by rememberSaveable { mutableStateOf(false) }
android.util.Log.d("MainActivity", "[STATE] TssPartyApp composing, startupComplete: $startupComplete")
// Handle success messages
LaunchedEffect(uiState.successMessage) {
@ -260,17 +297,30 @@ fun TssPartyApp(
navController.navigate("transfer/$shareId")
},
onExportBackup = { shareId, _ ->
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] ========== onExportBackup called ==========")
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] shareId: $shareId")
// Get address for filename
val share = shares.find { it.id == shareId }
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] share found: ${share != null}, address: ${share?.address}")
pendingExportAddress = share?.address
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] pendingExportAddress set to: $pendingExportAddress")
// Export and save to file
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] Calling viewModel.exportShareBackup...")
viewModel.exportShareBackup(shareId) { json ->
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] exportShareBackup callback received")
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] json length: ${json.length}")
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] Setting pendingExportJson...")
pendingExportJson = json
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] pendingExportJson set, length: ${pendingExportJson?.length}")
}
android.util.Log.d("MainActivity", "[EXPORT-TRIGGER] viewModel.exportShareBackup called (async)")
},
onImportBackup = {
android.util.Log.d("MainActivity", "[IMPORT-TRIGGER] ========== onImportBackup called ==========")
android.util.Log.d("MainActivity", "[IMPORT-TRIGGER] Launching file picker...")
// Open file picker to select backup file
openDocumentLauncher.launch(arrayOf("*/*"))
android.util.Log.d("MainActivity", "[IMPORT-TRIGGER] File picker launched")
},
onCreateWallet = {
navController.navigate(BottomNavItem.Create.route)

View File

@ -1958,18 +1958,39 @@ class TssRepository @Inject constructor(
* @return Result containing the backup JSON string
*/
suspend fun exportShareBackup(shareId: Long): Result<String> {
android.util.Log.d("TssRepository", "[EXPORT] ========== exportShareBackup START ==========")
android.util.Log.d("TssRepository", "[EXPORT] shareId: $shareId")
return withContext(Dispatchers.IO) {
try {
android.util.Log.d("TssRepository", "[EXPORT] Fetching share from database...")
val share = shareRecordDao.getShareById(shareId)
?: return@withContext Result.failure(Exception("钱包不存在"))
if (share == null) {
android.util.Log.e("TssRepository", "[EXPORT] Share not found in database!")
return@withContext Result.failure(Exception("钱包不存在"))
}
android.util.Log.d("TssRepository", "[EXPORT] Share found:")
android.util.Log.d("TssRepository", "[EXPORT] - address: ${share.address}")
android.util.Log.d("TssRepository", "[EXPORT] - sessionId: ${share.sessionId}")
android.util.Log.d("TssRepository", "[EXPORT] - partyId: ${share.partyId}")
android.util.Log.d("TssRepository", "[EXPORT] - partyIndex: ${share.partyIndex}")
android.util.Log.d("TssRepository", "[EXPORT] - threshold: ${share.thresholdT}-of-${share.thresholdN}")
android.util.Log.d("TssRepository", "[EXPORT] - publicKey length: ${share.publicKey.length}")
android.util.Log.d("TssRepository", "[EXPORT] - encryptedShare length: ${share.encryptedShare.length}")
android.util.Log.d("TssRepository", "[EXPORT] Converting to ShareBackup...")
val backup = ShareBackup.fromShareRecord(share.toShareRecord())
val json = com.google.gson.Gson().toJson(backup)
android.util.Log.d("TssRepository", "[EXPORT] ShareBackup created, version: ${backup.version}")
android.util.Log.d("TssRepository", "Exported share backup for address: ${share.address}")
android.util.Log.d("TssRepository", "[EXPORT] Serializing to JSON...")
val json = com.google.gson.Gson().toJson(backup)
android.util.Log.d("TssRepository", "[EXPORT] JSON length: ${json.length}")
android.util.Log.d("TssRepository", "[EXPORT] JSON preview: ${json.take(200)}...")
android.util.Log.d("TssRepository", "[EXPORT] ========== exportShareBackup SUCCESS ==========")
Result.success(json)
} catch (e: Exception) {
android.util.Log.e("TssRepository", "Failed to export share backup", e)
android.util.Log.e("TssRepository", "[EXPORT] ========== exportShareBackup FAILED ==========")
android.util.Log.e("TssRepository", "[EXPORT] Exception: ${e.javaClass.simpleName}: ${e.message}", e)
Result.failure(e)
}
}
@ -1981,19 +2002,41 @@ class TssRepository @Inject constructor(
* @return Result containing the imported ShareRecord
*/
suspend fun importShareBackup(backupJson: String): Result<ShareRecord> {
android.util.Log.d("TssRepository", "[IMPORT] ========== importShareBackup START ==========")
android.util.Log.d("TssRepository", "[IMPORT] JSON length: ${backupJson.length}")
android.util.Log.d("TssRepository", "[IMPORT] JSON preview: ${backupJson.take(200)}...")
return withContext(Dispatchers.IO) {
try {
android.util.Log.d("TssRepository", "[IMPORT] Parsing JSON to ShareBackup...")
val backup = com.google.gson.Gson().fromJson(backupJson, ShareBackup::class.java)
?: return@withContext Result.failure(Exception("无效的备份文件格式"))
if (backup == null) {
android.util.Log.e("TssRepository", "[IMPORT] Parsed backup is null!")
return@withContext Result.failure(Exception("无效的备份文件格式"))
}
android.util.Log.d("TssRepository", "[IMPORT] ShareBackup parsed successfully:")
android.util.Log.d("TssRepository", "[IMPORT] - version: ${backup.version}")
android.util.Log.d("TssRepository", "[IMPORT] - address: ${backup.address}")
android.util.Log.d("TssRepository", "[IMPORT] - sessionId: ${backup.sessionId}")
android.util.Log.d("TssRepository", "[IMPORT] - partyId: ${backup.partyId}")
android.util.Log.d("TssRepository", "[IMPORT] - partyIndex: ${backup.partyIndex}")
android.util.Log.d("TssRepository", "[IMPORT] - threshold: ${backup.thresholdT}-of-${backup.thresholdN}")
android.util.Log.d("TssRepository", "[IMPORT] - publicKey length: ${backup.publicKey.length}")
android.util.Log.d("TssRepository", "[IMPORT] - encryptedShare length: ${backup.encryptedShare.length}")
android.util.Log.d("TssRepository", "[IMPORT] - createdAt: ${backup.createdAt}")
android.util.Log.d("TssRepository", "[IMPORT] - exportedAt: ${backup.exportedAt}")
// Check if wallet already exists
android.util.Log.d("TssRepository", "[IMPORT] Checking if wallet already exists...")
val existingShare = shareRecordDao.getShareByAddress(backup.address)
if (existingShare != null) {
android.util.Log.e("TssRepository", "[IMPORT] Wallet already exists! id=${existingShare.id}")
return@withContext Result.failure(Exception("此钱包已存在 (地址: ${backup.address.take(10)}...)"))
}
android.util.Log.d("TssRepository", "[IMPORT] Wallet does not exist, proceeding with import")
// Convert to entity and save
// CRITICAL: Preserve the original partyId from backup - this is required for signing
android.util.Log.d("TssRepository", "[IMPORT] Converting to ShareRecordEntity...")
val shareRecord = backup.toShareRecord()
val entity = ShareRecordEntity(
sessionId = shareRecord.sessionId,
@ -2006,17 +2049,23 @@ class TssRepository @Inject constructor(
address = shareRecord.address,
createdAt = shareRecord.createdAt
)
android.util.Log.d("TssRepository", "[IMPORT] Entity created, partyId preserved: ${entity.partyId}")
android.util.Log.d("TssRepository", "[IMPORT] Inserting into database...")
val newId = shareRecordDao.insertShare(entity)
val savedShare = shareRecord.copy(id = newId)
android.util.Log.d("TssRepository", "[IMPORT] Inserted with id: $newId")
android.util.Log.d("TssRepository", "Imported share backup for address: ${backup.address}, partyId: ${backup.partyId}")
val savedShare = shareRecord.copy(id = newId)
android.util.Log.d("TssRepository", "[IMPORT] ========== importShareBackup SUCCESS ==========")
android.util.Log.d("TssRepository", "[IMPORT] Imported wallet: address=${backup.address}, partyId=${backup.partyId}")
Result.success(savedShare)
} catch (e: com.google.gson.JsonSyntaxException) {
android.util.Log.e("TssRepository", "Invalid JSON format in backup", e)
android.util.Log.e("TssRepository", "[IMPORT] ========== importShareBackup FAILED (JSON Error) ==========")
android.util.Log.e("TssRepository", "[IMPORT] JsonSyntaxException: ${e.message}", e)
Result.failure(Exception("备份文件格式错误"))
} catch (e: Exception) {
android.util.Log.e("TssRepository", "Failed to import share backup", e)
android.util.Log.e("TssRepository", "[IMPORT] ========== importShareBackup FAILED ==========")
android.util.Log.e("TssRepository", "[IMPORT] Exception: ${e.javaClass.simpleName}: ${e.message}", e)
Result.failure(e)
}
}

View File

@ -931,19 +931,30 @@ class MainViewModel @Inject constructor(
* @return The backup JSON string on success
*/
fun exportShareBackup(shareId: Long, onSuccess: (String) -> Unit) {
android.util.Log.d("MainViewModel", "[EXPORT] ========== exportShareBackup called ==========")
android.util.Log.d("MainViewModel", "[EXPORT] shareId: $shareId")
viewModelScope.launch {
android.util.Log.d("MainViewModel", "[EXPORT] Setting loading state...")
_exportResult.value = ExportImportResult(isLoading = true)
android.util.Log.d("MainViewModel", "[EXPORT] Calling repository.exportShareBackup...")
val result = repository.exportShareBackup(shareId)
android.util.Log.d("MainViewModel", "[EXPORT] Repository returned, isSuccess: ${result.isSuccess}")
result.fold(
onSuccess = { json ->
android.util.Log.d("MainViewModel", "[EXPORT] Export succeeded, json length: ${json.length}")
android.util.Log.d("MainViewModel", "[EXPORT] Setting success state and calling onSuccess callback...")
_exportResult.value = ExportImportResult(isSuccess = true)
android.util.Log.d("MainViewModel", "[EXPORT] Calling onSuccess callback with json...")
onSuccess(json)
android.util.Log.d("MainViewModel", "[EXPORT] onSuccess callback completed")
},
onFailure = { e ->
android.util.Log.e("MainViewModel", "[EXPORT] Export failed: ${e.message}", e)
_exportResult.value = ExportImportResult(error = e.message ?: "导出失败")
}
)
android.util.Log.d("MainViewModel", "[EXPORT] ========== exportShareBackup finished ==========")
}
}
@ -952,27 +963,42 @@ class MainViewModel @Inject constructor(
* @param backupJson The backup JSON string to import
*/
fun importShareBackup(backupJson: String) {
android.util.Log.d("MainViewModel", "[IMPORT] ========== importShareBackup called ==========")
android.util.Log.d("MainViewModel", "[IMPORT] JSON length: ${backupJson.length}")
android.util.Log.d("MainViewModel", "[IMPORT] JSON preview: ${backupJson.take(100)}...")
viewModelScope.launch {
android.util.Log.d("MainViewModel", "[IMPORT] Setting loading state...")
_importResult.value = ExportImportResult(isLoading = true)
android.util.Log.d("MainViewModel", "[IMPORT] Calling repository.importShareBackup...")
val result = repository.importShareBackup(backupJson)
android.util.Log.d("MainViewModel", "[IMPORT] Repository returned, isSuccess: ${result.isSuccess}")
result.fold(
onSuccess = { share ->
android.util.Log.d("MainViewModel", "[IMPORT] Import succeeded:")
android.util.Log.d("MainViewModel", "[IMPORT] - id: ${share.id}")
android.util.Log.d("MainViewModel", "[IMPORT] - address: ${share.address}")
android.util.Log.d("MainViewModel", "[IMPORT] - partyId: ${share.partyId}")
_importResult.value = ExportImportResult(
isSuccess = true,
message = "已成功导入钱包 (${share.address.take(10)}...)"
)
// Update wallet count
android.util.Log.d("MainViewModel", "[IMPORT] Updating wallet count...")
_appState.update { state ->
state.copy(walletCount = state.walletCount + 1)
}
// Fetch balance for the imported wallet
android.util.Log.d("MainViewModel", "[IMPORT] Fetching balance...")
fetchBalanceForShare(share)
android.util.Log.d("MainViewModel", "[IMPORT] Import complete!")
},
onFailure = { e ->
android.util.Log.e("MainViewModel", "[IMPORT] Import failed: ${e.message}", e)
_importResult.value = ExportImportResult(error = e.message ?: "导入失败")
}
)
android.util.Log.d("MainViewModel", "[IMPORT] ========== importShareBackup finished ==========")
}
}