From 7b3d28c95778574e8e17bd6a9f1c9f1920e071da Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 26 Jan 2026 05:39:56 -0800 Subject: [PATCH] =?UTF-8?q?feat(android):=20=E6=B7=BB=E5=8A=A0=E5=AF=BC?= =?UTF-8?q?=E5=87=BA/=E5=AF=BC=E5=85=A5=E5=A4=87=E4=BB=BD=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=9A=84=E8=AF=A6=E7=BB=86=E8=B0=83=E8=AF=95=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加日志位置: - 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 --- .../java/com/durian/tssparty/MainActivity.kt | 50 ++++++++++++++ .../tssparty/data/repository/TssRepository.kt | 67 ++++++++++++++++--- .../presentation/viewmodel/MainViewModel.kt | 26 +++++++ 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/MainActivity.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/MainActivity.kt index 0512022f..c7831253 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/MainActivity.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/MainActivity.kt @@ -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) diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt index 18bc9b2e..23485d4d 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt @@ -1958,18 +1958,39 @@ class TssRepository @Inject constructor( * @return Result containing the backup JSON string */ suspend fun exportShareBackup(shareId: Long): Result { + 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 { + 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) } } diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt index d40d38fa..bdb721f0 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt @@ -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 ==========") } }