Android 12 作用域存储迁移:公共SD卡与应用内部目录间大文件双向迁移的权限及实现方案问询
Alright, let's solve this big file move problem while sticking to Scoped Storage rules—since copying isn't feasible for large databases, we'll prioritize atomic moves where possible, and fall back to a memory-efficient copy-delete workflow when needed.
Key Background
Under Scoped Storage, you can't directly use File.moveTo() on files accessed via DocumentFile (since you don't have raw filesystem access to external directories outside your app's sandbox). Instead, we'll leverage the DocumentsContract API for true atomic moves (same storage device) and a chunked copy-delete approach for cross-storage scenarios.
Step 1: Persist Directory Permissions
First, make sure you retain the access permission for the picked directory so your app can use it beyond the current session. Update your onActivityResult handler with this:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == RESULT_OK && requestCode == DOCUMENT_TREE_ACTIVITY_REQUEST) { val treeUri = data?.data ?: return // Persist permission to access this directory across app restarts contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) val pickedDir = DocumentFile.fromTreeUri(this, treeUri) pickedDir?.listFiles()?.forEach { docFile -> if (docFile.isFile && docFile.name?.endsWith(".db") == true) { moveDbToAppStorage(this, docFile) } } } }
Step 2: Implement Move/Copy-Delete Logic
Create a helper function that first tries an atomic move (fast, no data duplication), then falls back to a memory-safe copy if the move fails (e.g., cross-storage device):
private fun moveDbToAppStorage(context: Context, sourceDoc: DocumentFile) { val contentResolver = context.contentResolver // Use app's external exclusive directory, fall back to internal storage if needed val targetDir = context.getExternalFilesDir(null) ?: context.filesDir val targetFile = File(targetDir, sourceDoc.name!!) // Handle existing target file (adjust this logic based on your app's needs) if (targetFile.exists() && !targetFile.delete()) { Log.e("DbMove", "Failed to delete existing database at target path") return } // 1. Attempt atomic move first (only works for same storage device) val sourceUri = sourceDoc.uri val targetDirDoc = DocumentFile.fromFile(targetDir) val targetDirUri = targetDirDoc.uri try { val movedUri = DocumentsContract.moveDocument( contentResolver, sourceUri, DocumentsContract.getTreeDocumentId(sourceUri), // Parent directory of source file targetDirUri // Target directory URI ) if (movedUri != null) { Log.d("DbMove", "Successfully moved database: ${sourceDoc.name}") return } } catch (e: Exception) { Log.w("DbMove", "Atomic move failed (likely cross-storage), falling back to copy-delete: ${e.message}") } // 2. Fallback: Chunked copy + delete source to avoid OOM with large files copyAndDeleteSource(contentResolver, sourceUri, targetFile, sourceDoc) } private fun copyAndDeleteSource( contentResolver: ContentResolver, sourceUri: Uri, targetFile: File, sourceDoc: DocumentFile ) { var inputStream: InputStream? = null var outputStream: OutputStream? = null try { inputStream = contentResolver.openInputStream(sourceUri) outputStream = FileOutputStream(targetFile) // Use 8KB buffer for chunked reading/writing (balances speed and memory usage) val buffer = ByteArray(8192) var bytesRead: Int while (inputStream.read(buffer).also { bytesRead = it } != -1) { outputStream.write(buffer, 0, bytesRead) } // Delete source file only after successful copy if (sourceDoc.delete()) { Log.d("DbMove", "Copied and deleted source database: ${targetFile.name}") } else { Log.w("DbMove", "Copy succeeded but failed to delete source file") } } catch (e: IOException) { Log.e("DbMove", "Error copying database: ${e.message}") // Clean up partial copy if something goes wrong if (targetFile.exists()) targetFile.delete() } finally { inputStream?.close() outputStream?.close() } }
Step 3: Reverse Operation (Export Database to SD Card)
For moving the app's internal database to /sdcard/Documents/myApp/db-export, follow the same pattern:
- Use
ACTION_OPEN_DOCUMENT_TREEto let the user select the export directory, then persist the permission. - Try
DocumentsContract.moveDocumentfirst (for same-storage moves), then fall back to the chunked copy-delete workflow.
Important Notes
- Atomic Move Limitation: The
moveDocumentAPI only works if the source and target are on the same storage device (e.g., both on the SD card, or both on internal storage). If they're on different devices, the API will fail, and we fall back to copy-delete. - Permission Persistence: Always call
takePersistableUriPermissionafter selecting a directory—otherwise, your app will lose access when it restarts. - Buffer Size: Adjust the 8KB buffer size based on your app's memory constraints; 4KB-16KB is generally safe for large files.
内容的提问来源于stack exchange,提问作者Luke

