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

Xamarin.Android无需All Files Access的文件夹选择器权限方案咨询

合规实现Android 12(API31)下SD卡自定义位置读写方案

针对你遇到的Google Play审核禁止MANAGE_EXTERNAL_STORAGE(All Files Access)权限,且传统存储权限无法访问SD卡自定义目录的问题,推荐使用**Storage Access Framework(SAF)**实现合规的自定义目录读写,这是Google官方推荐的替代方案,无需特殊权限,完全符合政策要求。

核心方案:使用SAF系统文件选择器

SAF允许用户主动授权应用访问指定的SD卡目录,应用通过获得的Uri进行文件操作,无需申请READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE或MANAGE_EXTERNAL_STORAGE权限。

1. 启动系统目录选择器,获取用户授权的SD卡目录Uri

private const val REQUEST_CODE_SELECT_DIR = 1001

fun launchDirectoryPicker() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
    // 添加权限标记,确保读写权限及持久化授权
    intent.addFlags(
        Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
        Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
    )
    startActivityForResult(intent, REQUEST_CODE_SELECT_DIR)
}

2. 处理选择结果,持久化保存授权Uri

将用户选择的目录Uri保存到SharedPreferences,避免每次启动重复授权:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_CODE_SELECT_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到本地存储
            getSharedPreferences("AppStoragePrefs", MODE_PRIVATE)
                .edit()
                .putString("selected_sd_dir_uri", treeUri.toString())
                .apply()
        }
    }
}

3. 通过ContentResolver进行目录创建与文件读写

创建自定义文件夹

fun createDataFolder(folderName: String): Uri? {
    val prefs = getSharedPreferences("AppStoragePrefs", MODE_PRIVATE)
    val treeUriString = prefs.getString("selected_sd_dir_uri", null) ?: return null
    val treeUri = Uri.parse(treeUriString)

    // 构建父目录的文档ID
    val parentDocId = DocumentsContract.getTreeDocumentId(treeUri)
    // 构建子目录的Uri
    val childDirUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocId)

    // 设置文件夹属性
    val folderValues = ContentValues().apply {
        put(DocumentsContract.Document.COLUMN_DISPLAY_NAME, folderName)
        put(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR)
    }

    // 插入并返回新文件夹的Uri
    return contentResolver.insert(childDirUri, folderValues)
}

写入数据文件

以写入生物数据CSV文件为例:

fun writeDataFile(folderUri: Uri, fileName: String, content: String) {
    val fileValues = ContentValues().apply {
        put(DocumentsContract.Document.COLUMN_DISPLAY_NAME, fileName)
        put(DocumentsContract.Document.COLUMN_MIME_TYPE, "text/csv")
    }

    // 在指定文件夹下创建文件
    val fileUri = contentResolver.insert(folderUri, fileValues) ?: return

    // 写入文件内容
    contentResolver.openOutputStream(fileUri)?.use { outputStream ->
        outputStream.write(content.toByteArray())
    }
}

关键说明

  • API31及以上,READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE仅能访问公共媒体目录(图片、视频等)和下载目录,无法访问SD卡自定义目录,因此必须使用SAF。
  • 无需申请任何特殊权限,SAF的授权完全由用户主动触发,符合Google Play隐私政策。
  • 持久化权限在应用卸载后会自动失效,重新安装需用户再次授权。
  • 可在应用首次启动时添加引导说明,告知用户选择SD卡目录的用途(野外数据存储、便于电脑读取),提升用户体验。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 05:34:58