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

API 30及以上版本如何访问与写入外置SD卡文件?

API 30及以上访问外置SD卡文件的解决方案

一、使用Storage Access Framework (SAF)操作外置SD卡文件

1. 唤起SD卡目录选择

启动ACTION_OPEN_DOCUMENT_TREE意图,引导用户选择外置SD卡的目标目录:

private const val REQUEST_CODE_SD_DIR = 1001

val selectDirIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(selectDirIntent, REQUEST_CODE_SD_DIR)

2. 持久化目录访问权限

用户选择目录后,在回调中获取目录Uri并申请持久化权限,避免每次启动重复选择:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_CODE_SD_DIR && resultCode == RESULT_OK) {
        data?.data?.let { treeUri ->
            // 申请读写权限并持久化
            contentResolver.takePersistableUriPermission(
                treeUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            )
            // 将Uri保存到SharedPreferences,后续复用
            getSharedPreferences("storage_prefs", MODE_PRIVATE)
                .edit()
                .putString("sd_tree_uri", treeUri.toString())
                .apply()
        }
    }
}

3. 读写指定格式文件

写入CSV/PDF文件

通过DocumentFile创建或获取文件,使用ContentResolver的流操作写入内容:

// 从SharedPreferences获取持久化的目录Uri
val treeUri = Uri.parse(
    getSharedPreferences("storage_prefs", MODE_PRIVATE)
        .getString("sd_tree_uri", "")
)
val rootDir = DocumentFile.fromTreeUri(this, treeUri) ?: return

// 写入CSV示例
val csvFile = rootDir.createFile("text/csv", "user_data.csv") ?: return
contentResolver.openOutputStream(csvFile.uri)?.use { output ->
    OutputStreamWriter(output).apply {
        write("ID,Name,Age\n1,Alice,28\n2,Bob,32")
        flush()
    }
}

// 写入PDF示例(假设已有PDF字节流)
val pdfFile = rootDir.createFile("application/pdf", "report.pdf") ?: return
contentResolver.openOutputStream(pdfFile.uri)?.use { output ->
    yourPdfByteArray?.let { output.write(it) }
}

操作DB文件

外置SD卡上的DB无法直接通过SQLiteOpenHelper访问,需先复制到内部临时目录操作,完成后写回:

// 从SD卡读取DB到内部临时文件
val dbFile = rootDir.findFile("app_data.db") ?: return
val tempDbPath = filesDir.path + "/temp_db.db"
contentResolver.openInputStream(dbFile.uri)?.use { input ->
    FileOutputStream(tempDbPath).use { output ->
        input.copyTo(output)
    }
}

// 操作临时DB
val db = SQLiteDatabase.openDatabase(tempDbPath, null, SQLiteDatabase.OPEN_READWRITE)
db.execSQL("INSERT INTO users(name) VALUES('Charlie')")
// ...其他DB操作
db.close()

// 将修改后的DB写回SD卡
contentResolver.openOutputStream(dbFile.uri)?.use { output ->
    FileInputStream(tempDbPath).use { input ->
        input.copyTo(output)
    }
}

// 删除临时文件
File(tempDbPath).delete()

二、MANAGE_EXTERNAL_STORAGE权限的补充说明

如果你的应用属于文件管理器、备份工具等符合Google政策的场景,可申请该权限直接访问外置SD卡所有文件:

  1. 清单文件添加权限:
<uses-permission 
    android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
    android:minSdkVersion="30" />
  1. 动态引导用户授权:
if (!Environment.isExternalStorageManager()) {
    val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
    intent.data = Uri.parse("package:$packageName")
    startActivity(intent)
}

注意:普通应用使用该权限可能无法通过Google Play审核,需谨慎选择。

关于SAF文档混乱的问题

确实,SAF的官方文档在版本迭代中存在描述不一致、示例缺失的情况,建议优先聚焦API 30+的官方存储章节,同时结合不同品牌设备测试——部分厂商的SD卡挂载逻辑会有差异,实际测试能更快定位问题。

内容的提问来源于stack exchange,提问作者lupaulus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 07:57:19