PAYATHON 2026

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)及以上版本中:

  1. 保留现有的所有文件访问权限检查。
  2. 调用 StorageManager.getStorageVolumes(),获取当前用户可见的存储卷。
  3. 检查各卷的挂载状态,通过 StorageVolume.getDirectory() 获取实际路径。
  4. 对实际目录调用 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。文件及目录访问文档

备注:内容仅供参考。