Replace share intent with SAF file save dialog for exports#425
Replace share intent with SAF file save dialog for exports#425torlando-tech merged 5 commits intomainfrom
Conversation
Data export and identity export now open Android's file save dialog (Storage Access Framework) instead of the share sheet, allowing users to save backup files directly to local storage. Fixes #344 https://claude.ai/code/session_01UXR4yz6pwyoNDDNXUPzz2s
Greptile SummaryReplaces Android's share intent with Storage Access Framework (SAF) file save dialog for both identity and data exports, giving users direct control over file save location. The implementation properly handles the export flow by storing temporary URIs and copying files to user-selected destinations.
Minor issue: Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Screen as UI Screen
participant ViewModel
participant SAF as Storage Access Framework
participant ContentResolver
participant FileSystem
User->>Screen: Click Export
Screen->>ViewModel: exportIdentity() / startExport()
ViewModel->>FileSystem: Create temp export file
FileSystem-->>ViewModel: Return temp URI
ViewModel->>ViewModel: Store in _exportedIdentityUri / _exportedFileUri
ViewModel->>Screen: Set ExportReady / ExportComplete state
Screen->>SAF: Launch CreateDocument dialog
SAF->>User: Show save file dialog
User->>SAF: Select location & filename
SAF-->>Screen: Return destination URI
Screen->>ViewModel: saveExportedIdentityToFile(uri) / saveExportToFile(uri)
ViewModel->>ContentResolver: openInputStream(sourceUri)
ViewModel->>ContentResolver: openOutputStream(destinationUri)
ContentResolver->>FileSystem: Copy file
FileSystem-->>ContentResolver: Success
ContentResolver-->>ViewModel: File copied
ViewModel->>ViewModel: cleanupExportFiles() [Migration only]
ViewModel->>Screen: Set Success / ExportSaved state
Screen->>User: Show success snackbar
Last reviewed commit: 9a0aeb1 |
app/src/main/java/com/lxmf/messenger/ui/screens/IdentityManagerScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/ui/screens/MigrationScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/ui/screens/IdentityManagerScreen.kt
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/ui/screens/MigrationScreen.kt
Outdated
Show resolved
Hide resolved
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…g change safety - Move file copy logic from composable callbacks into ViewModel methods (saveExportToFile, saveExportedIdentityToFile) so errors are reported via UiState → snackbar instead of being silently swallowed - Use ViewModel-held source URIs (_exportedFileUri, _exportedIdentityUri) which survive configuration changes, fixing state loss on rotation - Run file copy on Dispatchers.IO to avoid ANR on large exports https://claude.ai/code/session_01UXR4yz6pwyoNDDNXUPzz2s
… feedback - MigrationScreen: reset uiState after launching SAF save dialog to prevent it from re-triggering on configuration changes (rotation) - MigrationViewModel: add onExportSaveDialogLaunched() for state reset, add ExportSaved state, clean up temp file after successful save - IdentityManagerViewModel: show success snackbar after identity file is saved to user-chosen destination - MigrationScreen: show "Export saved successfully" snackbar on save Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
app/src/main/java/com/lxmf/messenger/viewmodel/IdentityManagerViewModel.kt
Show resolved
Hide resolved
| fun saveExportedIdentityToFile(destinationUri: Uri) { | ||
| val sourceUri = _exportedIdentityUri ?: return | ||
| viewModelScope.launch { | ||
| try { | ||
| withContext(Dispatchers.IO) { | ||
| context.contentResolver.openInputStream(sourceUri)?.use { input -> | ||
| context.contentResolver.openOutputStream(destinationUri)?.use { output -> | ||
| input.copyTo(output) | ||
| } | ||
| } ?: error("Could not open identity file") | ||
| } | ||
| _uiState.value = | ||
| IdentityManagerUiState.Success("Identity exported successfully") | ||
| } catch (e: Exception) { | ||
| Log.e(TAG, "Failed to save identity file", e) | ||
| _uiState.value = | ||
| IdentityManagerUiState.Error("Failed to save identity: ${e.message}") | ||
| } | ||
| } |
There was a problem hiding this comment.
_exportedIdentityUri is never cleared after the save operation completes. Consider adding _exportedIdentityUri = null after line 669 to prevent memory leaks from holding onto the URI reference
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/viewmodel/IdentityManagerViewModel.kt
Line: 652:670
Comment:
`_exportedIdentityUri` is never cleared after the save operation completes. Consider adding `_exportedIdentityUri = null` after line 669 to prevent memory leaks from holding onto the URI reference
How can I resolve this? If you propose a fix, please make it concise.…agerViewModel Merge main's encrypted import states (PasswordRequired, WrongPassword, password field on ImportPreview) alongside this branch's ExportSaved state. Also null out _exportedIdentityUri after save and add onCleared() cleanup to prevent holding stale URI references, matching MigrationViewModel's pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover saveExportToFile, onExportSaveDialogLaunched, and saveExportedIdentityToFile including success, failure, null URI, and URI cleanup after save scenarios. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| withContext(Dispatchers.IO) { | ||
| contentResolver.openInputStream(sourceUri)?.use { input -> | ||
| contentResolver.openOutputStream(destinationUri)?.use { output -> | ||
| input.copyTo(output) | ||
| } | ||
| } ?: error("Could not open export file") |
There was a problem hiding this comment.
Bug: A nested safe-call can cause a silent failure if openOutputStream() returns null, resulting in an empty export file without any error being reported.
Severity: CRITICAL
Suggested Fix
Explicitly check the result of contentResolver.openOutputStream(destinationUri). If it is null, throw an error to prevent the silent failure. This can be done by adding another elvis operator check for the inner use block, for example: ...?.use { output -> ... } ?: error("Could not open destination file").
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: app/src/main/java/com/lxmf/messenger/viewmodel/MigrationViewModel.kt#L306-L311
Potential issue: In `MigrationViewModel.saveExportToFile()` and
`IdentityManagerViewModel.saveExportedIdentityToFile()`, the file copying logic uses a
nested safe-call operator. If `contentResolver.openInputStream(sourceUri)` succeeds but
`contentResolver.openOutputStream(destinationUri)` returns null, the inner `?.use` block
is silently skipped. The `?: error(...)` at the end of the chain is not triggered
because the outer expression is not null. This causes the function to report a
successful export, but results in an empty file because no data was written, leading to
silent data loss.
Summary
Changed the export functionality in IdentityManagerScreen and MigrationScreen from using Android's share intent to using the Storage Access Framework (SAF) file save dialog. This provides users with direct control over where exported files are saved, rather than routing through a share sheet.
Key Changes
Intent.ACTION_SENDshare sheet withActivityResultContracts.CreateDocumentSAF dialog for identity exportsIntent.ACTION_SENDshare sheet withActivityResultContracts.CreateDocumentSAF dialog for data exportspendingExportSourceUristate variable to both screens to track the source file during the save operationcontentResolver.openInputStream()andopenOutputStream()to transfer data from temporary export location to user-selected destinationIntentimport from MigrationScreenImplementation Details
pendingExportSourceUriis cleared after the save operation completeshttps://claude.ai/code/session_01UXR4yz6pwyoNDDNXUPzz2s