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:
parent
c002640911
commit
7b3d28c957
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ==========")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue