You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android 12 作用域存储迁移:公共SD卡与应用内部目录间大文件双向迁移的权限及实现方案问询

Solution for Moving Large Files Under Android 12 Scoped Storage

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:

  1. Use ACTION_OPEN_DOCUMENT_TREE to let the user select the export directory, then persist the permission.
  2. Try DocumentsContract.moveDocument first (for same-storage moves), then fall back to the chunked copy-delete workflow.

Important Notes

  • Atomic Move Limitation: The moveDocument API 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 takePersistableUriPermission after 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 20:42:42