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读取数据库,不需要依赖本地文件绝对路径,实现步骤如下:
- 首次启动时通过系统文件选择器,引导用户授予目标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) } - 读取数据库时,直接通过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,除非核心功能是文件管理、数据备份类必须访问全量存储,否则会被直接拒审,仅适合自用或第三方渠道分发场景。
实现步骤:
- 在
AndroidManifest.xml中添加权限声明:<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> - 读取文件前先检查权限,未授权则跳转到系统授权页:
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

