Android 中读取 /storage 目录需要什么权限?
结论
如果已确认 Environment.isExternalStorageManager() 返回 true,就不需要再申请所谓的“读取 /storage”的普通应用权限。Android 没有专门的用户授权权限,能保证应用枚举 /storage 本身。
应通过 StorageManager 获取实际存储卷,再访问对应目录。/storage 是存储卷的挂载父目录,即使有“所有文件访问权限”,也不保证能列出这个父目录的内容。
为什么有“所有文件访问权限”仍然读不到
Environment.getStorageDirectory() 返回外部存储设备的挂载父目录,通常是 /storage。这个方法只返回一个 File 对象,不会读取目录内容,也不会授予权限。它与 Environment.getExternalStorageDirectory() 不同,后者返回的是主共享存储目录。Environment 官方文档
常见路径如下,实际位置应通过 API 获取:
/storage 存储卷的挂载父目录
/storage/emulated/0 当前用户的主共享存储目录
/storage/XXXX-XXXX 某个可移除存储卷的挂载目录
列出目录子项和沿已知路径访问子项是两种不同的操作。File("/storage").canRead() 返回 false,不能据此判断应用无法访问下面的实际存储卷。目录权限可以允许路径穿越,同时限制目录枚举;具体设备还受系统存储隔离等机制约束。
MANAGE_EXTERNAL_STORAGE 扩大了共享存储中的文件访问范围,包括 SD 卡和 USB OTG 存储卷的根目录,但不会授予整个文件系统的任意访问权。所有文件访问权限的范围
解决步骤:直接获取存储卷
在 Android 11(API 30)及以上版本中:
- 保留现有的所有文件访问权限检查。
- 调用
StorageManager.getStorageVolumes(),获取当前用户可见的存储卷。 - 检查各卷的挂载状态,通过
StorageVolume.getDirectory()获取实际路径。 - 对实际目录调用
listFiles(),分别处理读取失败和目录为空的情况。
getStorageVolumes() 返回的卷包括主共享存储以及已连接的 SD 卡、USB 存储卷,但这些卷不一定已经挂载。卷未挂载时,getDirectory() 可能返回 null。获取路径本身也不会额外授予访问权限。StorageManager 文档、StorageVolume 文档
Kotlin 示例
以下代码适用于已获得所有文件访问权限的应用。读取目录的操作应在后台线程执行。
import android.content.Context
import android.os.Build
import android.os.Environment
import android.os.storage.StorageManager
import android.util.Log
fun logStorageContents(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Log.w("Storage", "此示例要求 Android 11 或更高版本")
return
}
if (!Environment.isExternalStorageManager()) {
Log.w("Storage", "尚未获得所有文件访问权限")
return
}
val storageManager =
context.getSystemService(StorageManager::class.java)
for (volume in storageManager.storageVolumes) {
val state = volume.state
if (state != Environment.MEDIA_MOUNTED &&
state != Environment.MEDIA_MOUNTED_READ_ONLY
) {
continue
}
val directory = volume.directory ?: continue
try {
val entries = directory.listFiles()
when {
entries == null ->
Log.w("Storage", "无法列出目录:${directory.absolutePath}")
entries.isEmpty() ->
Log.d("Storage", "目录为空:${directory.absolutePath}")
else ->
entries.forEach { entry ->
Log.d("Storage", entry.absolutePath)
}
}
} catch (e: SecurityException) {
Log.e(
"Storage",
"访问被拒绝:${directory.absolutePath}",
e
)
}
}
}
不要用 listFiles().orEmpty() 判断读取是否成功,它会把返回 null 的失败情况也当成空目录。null 还可能表示目标不是目录或发生了 I/O 错误,不能仅凭这个返回值认定缺少权限。
注意事项
不要为了让 /storage 的 canRead() 返回 true,反复申请 READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE 或媒体读取权限。这些权限不能保证解除挂载父目录的枚举限制。
即使拥有 MANAGE_EXTERNAL_STORAGE,其他应用位于 Android/data/ 下的专属目录仍然受到限制。如果实际存储卷中的目标目录也无法读取,需要结合 Android 版本、targetSdkVersion、目标路径、挂载状态和具体错误继续排查。仅凭题目中的两个布尔结果,无法确定是哪一条底层限制导致的。
如果只需要让用户选择一个文件或文件夹,可以使用 Storage Access Framework,通过返回的 URI 访问内容。ACTION_OPEN_DOCUMENT_TREE 也有目录选择限制,不能保证授权访问 /storage 或任意存储卷根目录,也不会让 File("/storage").canRead() 变成 true。文件及目录访问文档
备注:内容仅供参考。
