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

Android 12读取内部存储Keepass文件报EACCES权限拒绝问题

问题根因

你遇到的EACCES (Permission denied)是Android分区存储(Scoped Storage)机制的限制:从Android 11开始,普通应用即便动态申请了READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE权限,也无法直接通过File类的绝对路径访问共享存储根目录下的任意文件,和你了解的Android 12存储权限调整逻辑一致。

以下是两种不需要移动原文件、不影响群晖NAS同步逻辑的可行方案,按推荐优先级排序:


方案1:使用SAF存储访问框架读取(合规、推荐)

这是官方推荐的实现方式,兼容Android 11~14全版本,不需要申请高敏感权限,也不会影响原文件的存储位置和同步逻辑。
你用的openkeepass库本身就支持传入InputStream读取数据库,不需要依赖本地文件绝对路径,实现步骤如下:

  1. 首次启动时通过系统文件选择器,引导用户授予目标kdbx文件的持久化访问权限,后续打开不需要重复授权:
    // 注册文件选择器回调
    private val kdbxFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            result.data?.data?.also { uri ->
                // 申请持久化访问权限,应用重启后依然有效
                contentResolver.takePersistableUriPermission(
                    uri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION
                )
                // 将Uri持久化存储到本地,下次启动直接读取
                getSharedPreferences("app_config", MODE_PRIVATE).edit()
                    .putString("kdbx_file_uri", uri.toString())
                    .apply()
                // 读取数据库
                readKdbx(uri)
            }
        }
    }
    
    // 需要选择文件时调用该方法拉起选择器,可直接定位到Keepass文件夹减少用户操作
    private fun pickKdbxFile() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "*/*"
            // 初始定位到内部存储的Keepass目录
            putExtra(
                DocumentsContract.EXTRA_INITIAL_URI,
                Uri.parse("content://com.android.externalstorage.documents/document/primary:Keepass")
            )
        }
        kdbxFilePicker.launch(intent)
    }
    
  2. 读取数据库时,直接通过Uri获取输入流传给openkeepass库,不要构造File对象:
    private fun readKdbx(fileUri: Uri) {
        try {
            contentResolver.openInputStream(fileUri)?.use { inputStream ->
                // 直接传入输入流读取,不需要本地文件路径
                val database = KeePassDatabase.getInstance(inputStream).openDatabase("你的数据库密码")
                val entries = database.entries
                entries.forEach { entry ->
                    Log.d("[KDBX Read]", "Entry: ${entry.title}")
                }
            }
        } catch (e: Exception) {
            Toast.makeText(applicationContext, "打开密码库失败", Toast.LENGTH_SHORT).show()
            e.printStackTrace()
        }
    }
    

这个方案完全符合Google Play的上架规则,不会有权限合规问题,原文件始终保留在/storage/emulated/0/Keepass路径下,群晖同步逻辑不受任何影响。


方案2:申请所有文件访问权限(适合自用、不推荐上架使用)

如果你不想做文件选择的交互,就想直接通过绝对路径读取文件,可以申请MANAGE_EXTERNAL_STORAGE全文件访问权限,但注意:该权限属于高敏感权限,如果你的应用需要上架Google Play,除非核心功能是文件管理、数据备份类必须访问全量存储,否则会被直接拒审,仅适合自用或第三方渠道分发场景。
实现步骤:

  1. 在AndroidManifest.xml中添加权限声明:
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    
  2. 读取文件前先检查权限,未授权则跳转到系统授权页:
    if (!Environment.isExternalStorageManager()) {
        val permIntent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
            data = Uri.parse("package:$packageName")
        }
        startActivity(permIntent)
        return
    }
    // 权限通过后,你原来写的通过绝对路径构造File读取的代码可以直接正常运行
    

避坑提示

不要在Android 11+版本上浪费时间调试READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE权限,这两个权限在分区存储规则下,只能访问应用专属目录、公共媒体目录(相册、音频、视频)的文件,无法访问kdbx这类非媒体格式的普通文件,就算权限申请通过,用绝对路径读取依然会抛EACCES异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 21:18:28