如何获得每个StorageVolume的免费和总大小?

and*_*per 17 android storage-access-framework

背景

Google不幸地计划破坏存储权限,以使应用程序无法使用标准File API(和文件路径)访问文件系统。许多人反对它,因为它改变了应用程序访问存储的方式,并且在许多方面都是受限制的有限API。

因此,如果我们想处理各种问题,我们将需要在将来的某些Android版本上完全使用SAF(存储访问框架)(在Android Q上,我们至少可以暂时使用一个标志来使用常规存储权限)。存储卷并到达那里的所有文件。

因此,例如,假设您要制作一个文件管理器并显示设备的所有存储卷,并为每个文件显示有多少总字节和可用字节。这样的事情看起来非常合法,但是由于我找不到找到这种方法的方法。

问题

从API 24(此处)开始,我们终于可以列出所有存储卷,如下所示:

    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes
Run Code Online (Sandbox Code Playgroud)

事实是,此列表上的每个项目都没有功能来获取其大小和可用空间。

但是,以某种方式,Google的 Google提供的文件”应用程序设法获得了该信息,而没有获得任何形式的许可:

在此处输入图片说明

并在装有Android 8的Galaxy Note 8上进行了测试。甚至没有最新版本的Android。

因此,这意味着即使在Android 8上,也应该有一种无需任何许可即可获取此信息的方法。

我发现了什么

有一些类似于获得自由空间的东西,但是我不确定是否确实如此。虽然如此。这是它的代码:

    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes
    AsyncTask.execute {
        for (storageVolume in storageVolumes) {
            val uuid: UUID = storageVolume.uuid?.let { UUID.fromString(it) } ?: StorageManager.UUID_DEFAULT
            val allocatableBytes = storageManager.getAllocatableBytes(uuid)
            Log.d("AppLog", "allocatableBytes:${android.text.format.Formatter.formatShortFileSize(this,allocatableBytes)}")
        }
    }
Run Code Online (Sandbox Code Playgroud)

但是,我找不到类似的东西来获取每个StorageVolume实例的总空间。假设我对此是正确的,我已经在这里提出了要求。

您可以在我对此问题写的答案中找到更多我所找到的内容,但是目前,这是所有变通办法的结合,这些工作不是变通办法,而是在某些情况下有效。

问题

  1. 是否getAllocatableBytes确实得到了自由空间的方式吗?
  2. 我如何获得每个StorageVolume的可用空间和实际总空间(在某些情况下,由于某些原因我获得了较低的值),而无需请求任何许可,就像在Google的应用程序上一样?

Che*_*amp 9

getAllocableBytes 确实是获得可用空间的方法吗?

Android 8.0 功能和 API声明getAllocableBytes(UUID)

最后,当您需要为大文件分配磁盘空间时,请考虑使用新的 allocateBytes(FileDescriptor, long) API,它会自动清除属于其他应用程序的缓存文件(根据需要)以满足您的请求。在决定设备是否有足够的磁盘空间来保存您的新数据时,请调用 getAllocableBytes(UUID) 而不是使用 getUsableSpace(),因为前者会考虑系统愿意代表您清除的任何缓存数据。

因此,getAllocableBytes()通过清除其他应用程序的缓存来报告新文件可以空闲的字节数,但当前可能不是空闲的。这似乎不是对通用文件实用程序的正确调用。

在任何情况下,getAllocableBytes(UUID)似乎都不适用于主卷以外的任何卷,因为无法从StorageManager获取可接受的 UUID以用于主卷以外的存储卷。请参阅从 Android StorageManager 获得的存储 UUID 无效?错误报告 #62982912。(这里提到的完整性;我知道你已经知道这些。)错误报告现在已经两年多了,没有解决方案或暗示解决方法,所以没有爱。

如果您想要“Google 文件”或其他文件管理器报告的可用空间类型,那么您将需要以不同的方式处理可用空间,如下所述。

如何获得每个 StorageVolume 的免费和实际总空间(在某些情况下,由于某些原因我得到了较低的值),而无需请求任何许可,就像在 Google 的应用程序上一样?

以下是获取可用卷的可用空间和总空间的过程:

识别外部目录:使用getExternalFilesDirs(null)来发现可用的外部位置。返回的是File[]。这些是我们的应用程序被允许使用的目录。

extDirs = {File 2 @9489
0 = {File@9509} "/storage/emulated/0/Android/data/com.example.storagevolumes/files"
1 = {File@9510} "/storage/14E4-120B/Android /data/com.example.storagevolumes/files”

(注意根据文档,此调用返回被认为是稳定设备的设备,例如 SD 卡。这不会返回连接的 USB 驱动器。)

识别存储卷:对于上面返回的每个目录,使用StorageManager#getStorageVolume(File)来识别包含该目录的存储卷。我们不需要识别顶级目录来获取存储卷,只需从存储卷中提取一个文件,因此这些目录就可以了。

计算总空间和已用空间:确定存储卷上的空间。主卷的处理方式与 SD 卡不同。

对于主卷:使用StorageStatsManager#getTotalBytes(UUID使用StorageManager#UUID_DEFAULT 获取主设备上的标称总存储字节。返回的值将千字节视为 1,000 字节(而不是 1,024),将千兆字节视为 1,000,000,000 字节而不是 2 30 . 在我的三星 Galaxy S7 上,报告的值为 32,000,000,000 字节。在我的 Pixel 3 模拟器上,运行 API 29 并具有 16 MB 的存储空间,报告的值为 16,000,000,000。

诀窍如下:如果您想要“Google 文件”报告的数字,请使用 10 3表示 1 千字节,10 6表示 1 兆字节,10 9表示 1 千兆字节。对于其他文件管理器 2 10、 2 20和 2 30是有效的。(这将在下面证明。)请参阅关于这些单元的更多信息。

要获得免费字节,请使用StorageStatsManager#getFreeBytes(uuid)。已用字节数是总字节数与空闲字节数之差。

对于非主卷:非主卷的空间计算很简单:对于使用的总空间File#getTotalSpaceFile#getFreeSpace的可用空间。

下面是几个显示音量统计信息的屏幕截图。第一张图片显示了StorageVolumeStats应用程序(包含在图片下方)和“Google 文件”的输出。顶部部分顶部的切换按钮在使用 1,000 和 1,024 千字节之间切换应用程序。如您所见,数字一致。(这是运行 Oreo 的设备的屏幕截图。我无法将“Google 文件”的测试版加载到 Android Q 模拟器上。)

在此处输入图片说明

下图显示了顶部的StorageVolumeStats应用程序和底部的“EZ 文件资源管理器”的输出。此处 1,024 用于表示千字节,两个应用程序就可用空间的总数和可用空间达成一致(四舍五入除外)。

在此处输入图片说明

主活动.kt

这个小应用程序只是主要活动。清单是通用的,compileSdkVersiontargetSdkVersion被设置为29的minSdkVersion是26。

class MainActivity : AppCompatActivity() {
    private lateinit var mStorageManager: StorageManager
    private val mStorageVolumesByExtDir = mutableListOf<VolumeStats>()
    private lateinit var mVolumeStats: TextView
    private lateinit var mUnitsToggle: ToggleButton
    private var mKbToggleValue = true
    private var kbToUse = KB
    private var mbToUse = MB
    private var gbToUse = GB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState != null) {
            mKbToggleValue = savedInstanceState.getBoolean("KbToggleValue", true)
            selectKbValue()
        }
        setContentView(statsLayout())

        mStorageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager

        getVolumeStats()
        showVolumeStats()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putBoolean("KbToggleValue", mKbToggleValue)
    }

    private fun getVolumeStats() {
        // We will get our volumes from the external files directory list. There will be one
        // entry per external volume.
        val extDirs = getExternalFilesDirs(null)

        mStorageVolumesByExtDir.clear()
        extDirs.forEach { file ->
            val storageVolume: StorageVolume? = mStorageManager.getStorageVolume(file)
            if (storageVolume == null) {
                Log.d(TAG, "Could not determinate StorageVolume for ${file.path}")
            } else {
                val totalSpace: Long
                val usedSpace: Long
                if (storageVolume.isPrimary) {
                    // Special processing for primary volume. "Total" should equal size advertised
                    // on retail packaging and we get that from StorageStatsManager. Total space
                    // from File will be lower than we want to show.
                    val uuid = StorageManager.UUID_DEFAULT
                    val storageStatsManager =
                        getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
                    // Total space is reported in round numbers. For example, storage on a
                    // SamSung Galaxy S7 with 32GB is reported here as 32_000_000_000. If
                    // true GB is needed, then this number needs to be adjusted. The constant
                    // "KB" also need to be changed to reflect KiB (1024).
//                    totalSpace = storageStatsManager.getTotalBytes(uuid)
                    totalSpace = (storageStatsManager.getTotalBytes(uuid) / 1_000_000_000) * gbToUse
                    usedSpace = totalSpace - storageStatsManager.getFreeBytes(uuid)
                } else {
                    // StorageStatsManager doesn't work for volumes other than the primary volume
                    // since the "UUID" available for non-primary volumes is not acceptable to
                    // StorageStatsManager. We must revert to File for non-primary volumes. These
                    // figures are the same as returned by statvfs().
                    totalSpace = file.totalSpace
                    usedSpace = totalSpace - file.freeSpace
                }
                mStorageVolumesByExtDir.add(
                    VolumeStats(storageVolume, totalSpace, usedSpace)
                )
            }
        }
    }

    private fun showVolumeStats() {
        val sb = StringBuilder()
        mStorageVolumesByExtDir.forEach { volumeStats ->
            val (usedToShift, usedSizeUnits) = getShiftUnits(volumeStats.mUsedSpace)
            val usedSpace = (100f * volumeStats.mUsedSpace / usedToShift).roundToLong() / 100f
            val (totalToShift, totalSizeUnits) = getShiftUnits(volumeStats.mTotalSpace)
            val totalSpace = (100f * volumeStats.mTotalSpace / totalToShift).roundToLong() / 100f
            val uuidToDisplay: String?
            val volumeDescription =
                if (volumeStats.mStorageVolume.isPrimary) {
                    uuidToDisplay = ""
                    PRIMARY_STORAGE_LABEL
                } else {
                    uuidToDisplay = " (${volumeStats.mStorageVolume.uuid})"
                    volumeStats.mStorageVolume.getDescription(this)
                }
            sb
                .appendln("$volumeDescription$uuidToDisplay")
                .appendln(" Used space: ${usedSpace.nice()} $usedSizeUnits")
                .appendln("Total space: ${totalSpace.nice()} $totalSizeUnits")
                .appendln("----------------")
        }
        mVolumeStats.text = sb.toString()
    }

    private fun getShiftUnits(x: Long): Pair<Long, String> {
        val usedSpaceUnits: String
        val shift =
            when {
                x < kbToUse -> {
                    usedSpaceUnits = "Bytes"; 1L
                }
                x < mbToUse -> {
                    usedSpaceUnits = "KB"; kbToUse
                }
                x < gbToUse -> {
                    usedSpaceUnits = "MB"; mbToUse
                }
                else -> {
                    usedSpaceUnits = "GB"; gbToUse
                }
            }
        return Pair(shift, usedSpaceUnits)
    }

    @SuppressLint("SetTextI18n")
    private fun statsLayout(): SwipeRefreshLayout {
        val swipeToRefresh = SwipeRefreshLayout(this)
        swipeToRefresh.setOnRefreshListener {
            getVolumeStats()
            showVolumeStats()
            swipeToRefresh.isRefreshing = false
        }

        val scrollView = ScrollView(this)
        swipeToRefresh.addView(scrollView)
        val linearLayout = LinearLayout(this)
        linearLayout.orientation = LinearLayout.VERTICAL
        scrollView.addView(
            linearLayout, ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )

        val instructions = TextView(this)
        instructions.text = "Swipe down to refresh."
        linearLayout.addView(
            instructions, ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
        (instructions.layoutParams as LinearLayout.LayoutParams).gravity = Gravity.CENTER

        mUnitsToggle = ToggleButton(this)
        mUnitsToggle.textOn = "KB = 1,000"
        mUnitsToggle.textOff = "KB = 1,024"
        mUnitsToggle.isChecked = mKbToggleValue
        linearLayout.addView(
            mUnitsToggle, ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
        mUnitsToggle.setOnClickListener { v ->
            val toggleButton = v as ToggleButton
            mKbToggleValue = toggleButton.isChecked
            selectKbValue()
            getVolumeStats()
            showVolumeStats()
        }

        mVolumeStats = TextView(this)
        mVolumeStats.typeface = Typeface.MONOSPACE
        val padding =
            16 * (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT).toInt()
        mVolumeStats.setPadding(padding, padding, padding, padding)

        val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0)
        lp.weight = 1f
        linearLayout.addView(mVolumeStats, lp)

        return swipeToRefresh
    }

    private fun selectKbValue() {
        if (mKbToggleValue) {
            kbToUse = KB
            mbToUse = MB
            gbToUse = GB
        } else {
            kbToUse = KiB
            mbToUse = MiB
            gbToUse = GiB
        }
    }

    companion object {
        fun Float.nice(fieldLength: Int = 6): String =
            String.format(Locale.US, "%$fieldLength.2f", this)

        // StorageVolume should have an accessible "getPath()" method that will do
        // the following so we don't have to resort to reflection.
        @Suppress("unused")
        fun StorageVolume.getStorageVolumePath(): String {
            return try {
                javaClass
                    .getMethod("getPath")
                    .invoke(this) as String
            } catch (e: Exception) {
                e.printStackTrace()
                ""
            }
        }

        // See https://en.wikipedia.org/wiki/Kibibyte for description
        // of these units.

        // These values seems to work for "Files by Google"...
        const val KB = 1_000L
        const val MB = KB * KB
        const val GB = KB * KB * KB

        // ... and these values seems to work for other file manager apps.
        const val KiB = 1_024L
        const val MiB = KiB * KiB
        const val GiB = KiB * KiB * KiB

        const val PRIMARY_STORAGE_LABEL = "Internal Storage"

        const val TAG = "MainActivity"
    }

    data class VolumeStats(
        val mStorageVolume: StorageVolume,
        var mTotalSpace: Long = 0,
        var mUsedSpace: Long = 0
    )
}
Run Code Online (Sandbox Code Playgroud)

附录

让我们更习惯使用getExternalFilesDirs()

我们 在代码中调用Context#getExternalFilesDirs()。在此方法中,调用Environment#buildExternalStorageAppFilesDirs()调用Environment#getExternalDirs()StorageManager获取卷列表。该存储列表用于创建我们看到的从Context#getExternalFilesDirs()返回的路径,方法是将一些静态路径段附加到每个存储卷标识的路径。

我们真的希望访问Environment#getExternalDirs()以便我们可以立即确定空间利用率,但我们受到限制。由于我们进行的调用依赖于从卷列表生成的文件列表,我们可以放心,所有卷都被输出代码覆盖,我们可以获得所需的空间利用率信息。


and*_*per 5

找到了一种解决方法,使用我在这里写的内容,并将每个 StorageVolume 映射到我在这里写的真实文件。遗憾的是,这在未来可能行不通,因为它使用了很多“技巧”:

        for (storageVolume in storageVolumes) {
            val volumePath = FileUtilEx.getVolumePath(storageVolume)
            if (volumePath == null) {
                Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - failed to get volumePath")
            } else {
                val statFs = StatFs(volumePath)
                val availableSizeInBytes = statFs.availableBytes
                val totalBytes = statFs.totalBytes
                val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
                Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - volumePath:$volumePath - $formattedResult")
            }
        }
Run Code Online (Sandbox Code Playgroud)

似乎可以在模拟器(具有主存储和 SD 卡)和真实设备(Pixel 2)上运行,两者都运行 Android Q beta 4。

一个更好的解决方案,不使用反射,可能是在我们进入的每个路径中放置一个唯一的文件ContextCompat.getExternalCacheDirs,然后尝试通过每个 StorageVolume 实例找到它们。但这很棘手,因为您不知道何时开始搜索,因此您需要检查各种路径,直到到达目的地。不仅如此,正如我在这里所写的,我认为没有一种官方方法可以获取每个 StorageVolume 的 Uri 或 DocumentFile 或文件或文件路径。

不管怎样,奇怪的是总空间比真实的要低。可能是因为它是用户真正可用的最大值的一部分。

我想知道各种应用程序(例如文件管理器应用程序,如 Total Commander)如何获得真正的总设备存储空间。


编辑:好的,有另一种解决方法,它可能更可靠,基于storageManager.getStorageVolume(File)函数。

因此,这是两种解决方法的合并:

fun getStorageVolumePath(context: Context, storageVolumeToGetItsPath: StorageVolume): String? {
    //first, try to use reflection
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
        return null
    try {
        val storageVolumeClazz = StorageVolume::class.java
        val getPathMethod = storageVolumeClazz.getMethod("getPath")
        val result = getPathMethod.invoke(storageVolumeToGetItsPath) as String?
         if (!result.isNullOrBlank())
            return result
    } catch (e: Exception) {
        e.printStackTrace()
    }
    //failed to use reflection, so try mapping with app's folders
    val storageVolumeUuidStr = storageVolumeToGetItsPath.uuid
    val externalCacheDirs = ContextCompat.getExternalCacheDirs(context)
    val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
    for (externalCacheDir in externalCacheDirs) {
        val storageVolume = storageManager.getStorageVolume(externalCacheDir) ?: continue
        val uuidStr = storageVolume.uuid
        if (uuidStr == storageVolumeUuidStr) {
            //found storageVolume<->File match
            var resultFile = externalCacheDir
            while (true) {
                val parentFile = resultFile.parentFile ?: return resultFile.absolutePath
                val parentFileStorageVolume = storageManager.getStorageVolume(parentFile)
                        ?: return resultFile.absolutePath
                if (parentFileStorageVolume.uuid != uuidStr)
                    return resultFile.absolutePath
                resultFile = parentFile
            }
        }
    }
    return null
}
Run Code Online (Sandbox Code Playgroud)

为了显示可用空间和总空间,我们像以前一样使用 StatFs:

for (storageVolume in storageVolumes) {
    val storageVolumePath = getStorageVolumePath(this@MainActivity, storageVolume) ?: continue
    val statFs = StatFs(storageVolumePath)
    val availableSizeInBytes = statFs.availableBytes
    val totalBytes = statFs.totalBytes
    val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
    Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - storageVolumePath:$storageVolumePath - $formattedResult")
}
Run Code Online (Sandbox Code Playgroud)

编辑:较短的版本,不使用存储卷的真实文件路径:

fun getStatFsForStorageVolume(context: Context, storageVolumeToGetItsPath: StorageVolume): StatFs? {
    //first, try to use reflection
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
        return null
    try {
        val storageVolumeClazz = StorageVolume::class.java
        val getPathMethod = storageVolumeClazz.getMethod("getPath")
        val resultPath = getPathMethod.invoke(storageVolumeToGetItsPath) as String?
        if (!resultPath.isNullOrBlank())
            return StatFs(resultPath)
    } catch (e: Exception) {
        e.printStackTrace()
    }
    //failed to use reflection, so try mapping with app's folders
    val storageVolumeUuidStr = storageVolumeToGetItsPath.uuid
    val externalCacheDirs = ContextCompat.getExternalCacheDirs(context)
    val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
    for (externalCacheDir in externalCacheDirs) {
        val storageVolume = storageManager.getStorageVolume(externalCacheDir) ?: continue
        val uuidStr = storageVolume.uuid
        if (uuidStr == storageVolumeUuidStr) {
            //found storageVolume<->File match
            return StatFs(externalCacheDir.absolutePath)
        }
    }
    return null
}
Run Code Online (Sandbox Code Playgroud)

用法:

        for (storageVolume in storageVolumes) {
            val statFs = getStatFsForStorageVolume(this@MainActivity, storageVolume)
                    ?: continue
            val availableSizeInBytes = statFs.availableBytes
            val totalBytes = statFs.totalBytes
            val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
            Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - $formattedResult")
        }
Run Code Online (Sandbox Code Playgroud)

请注意,此解决方案不需要任何类型的许可。

--

编辑:我实际上发现我过去曾尝试这样做,但由于某种原因它在模拟器上的 SD 卡 StoraveVolume 上崩溃了:

        val storageStatsManager = getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
        for (storageVolume in storageVolumes) {
            val uuidStr = storageVolume.uuid
            val uuid = if (uuidStr == null) StorageManager.UUID_DEFAULT else UUID.fromString(uuidStr)
            val availableSizeInBytes = storageStatsManager.getFreeBytes(uuid)
            val totalBytes = storageStatsManager.getTotalBytes(uuid)
            val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
            Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - $formattedResult")
        }
Run Code Online (Sandbox Code Playgroud)

好消息是,对于主存储卷,您可以获得它的真实总空间。

在真实设备上,SD 卡也会崩溃,但主卡不会崩溃。


因此,这是收集上述内容的最新解决方案:

        for (storageVolume in storageVolumes) {
            val availableSizeInBytes: Long
            val totalBytes: Long
            if (storageVolume.isPrimary) {
                val storageStatsManager = getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
                val uuidStr = storageVolume.uuid
                val uuid = if (uuidStr == null) StorageManager.UUID_DEFAULT else UUID.fromString(uuidStr)
                availableSizeInBytes = storageStatsManager.getFreeBytes(uuid)
                totalBytes = storageStatsManager.getTotalBytes(uuid)
            } else {
                val statFs = getStatFsForStorageVolume(this@MainActivity, storageVolume)
                        ?: continue
                availableSizeInBytes = statFs.availableBytes
                totalBytes = statFs.totalBytes
            }
            val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
            Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - $formattedResult")
        }
Run Code Online (Sandbox Code Playgroud)

Android R 的更新答案:

        fun getStorageVolumesAccessState(context: Context) {
            val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
            val storageVolumes = storageManager.storageVolumes
            val storageStatsManager = context.getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
            for (storageVolume in storageVolumes) {
                var freeSpace: Long = 0L
                var totalSpace: Long = 0L
                val path = getPath(context, storageVolume)
                if (storageVolume.isPrimary) {
                    totalSpace = storageStatsManager.getTotalBytes(StorageManager.UUID_DEFAULT)
                    freeSpace = storageStatsManager.getFreeBytes(StorageManager.UUID_DEFAULT)
                } else if (path != null) {
                    val file = File(path)
                    freeSpace = file.freeSpace
                    totalSpace = file.totalSpace
                }
                val usedSpace = totalSpace - freeSpace
                val freeSpaceStr = Formatter.formatFileSize(context, freeSpace)
                val totalSpaceStr = Formatter.formatFileSize(context, totalSpace)
                val usedSpaceStr = Formatter.formatFileSize(context, usedSpace)
                Log.d("AppLog", "${storageVolume.getDescription(context)} - path:$path total:$totalSpaceStr used:$usedSpaceStr free:$freeSpaceStr")
            }
        }

        fun getPath(context: Context, storageVolume: StorageVolume): String? {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
                storageVolume.directory?.absolutePath?.let { return it }
            try {
                return storageVolume.javaClass.getMethod("getPath").invoke(storageVolume) as String
            } catch (e: Exception) {
            }
            try {
                return (storageVolume.javaClass.getMethod("getPathFile").invoke(storageVolume) as File).absolutePath
            } catch (e: Exception) {
            }
            val extDirs = context.getExternalFilesDirs(null)
            for (extDir in extDirs) {
                val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
                val fileStorageVolume: StorageVolume = storageManager.getStorageVolume(extDir)
                        ?: continue
                if (fileStorageVolume == storageVolume) {
                    var file = extDir
                    while (true) {
                        val parent = file.parentFile ?: return file.absolutePath
                        val parentStorageVolume = storageManager.getStorageVolume(parent)
                                ?: return file.absolutePath
                        if (parentStorageVolume != storageVolume)
                            return file.absolutePath
                        file = parent
                    }
                }
            }
            try {
                val parcel = Parcel.obtain()
                storageVolume.writeToParcel(parcel, 0)
                parcel.setDataPosition(0)
                parcel.readString()
                return parcel.readString()
            } catch (e: Exception) {
            }
            return null
        }
Run Code Online (Sandbox Code Playgroud)


Che*_*amp 5

以下用于fstatvfs(FileDescriptor)检索统计信息而无需借助反射或传统文件系统方法。

为了检查程序的输出以确保它产生合理的总空间,已使用空间和可用空间,我在运行API 29的Android模拟器上运行了“ df”命令。

adb shell中“ df”命令的输出报告1K块:

当StorageVolume#isPrimary为true时,“ / data”对应于“主要” UUID。

“ / storage / 1D03-2E0E”对应于StorageVolume#uuid报告的“ 1D03-2E0E” UUID。

generic_x86:/ $ df
Filesystem              1K-blocks    Used Available Use% Mounted on
/dev/root                 2203316 2140872     46060  98% /
tmpfs                     1020140     592   1019548   1% /dev
tmpfs                     1020140       0   1020140   0% /mnt
tmpfs                     1020140       0   1020140   0% /apex
/dev/block/vde1            132168   75936     53412  59% /vendor

/dev/block/vdc             793488  647652    129452  84% /data

/dev/block/loop0              232      36       192  16% /apex/com.android.apex.cts.shim@1
/data/media                793488  647652    129452  84% /storage/emulated

/mnt/media_rw/1D03-2E0E    522228      90    522138   1% /storage/1D03-2E0E
Run Code Online (Sandbox Code Playgroud)

应用使用fstatvfs报告(以1K块为单位):

对于/ tree / primary:/ document / primary:总计= 793,488已使用空间= 647,652可用= 129,452

对于/ tree / 1D03-2E0E:/ document / 1D03-2E0E:总计= 522,228已使用空间= 90可用= 522,138

总计匹配。

fstatvfs 在这里描述。

有关fstatvfs返回的详细信息,请参见此处

以下小应用程序显示可访问卷的已使用字节,可用字节和总字节。

在此处输入图片说明

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var mStorageManager: StorageManager
    private val mVolumeStats = HashMap<Uri, StructStatVfs>()
    private val mStorageVolumePathsWeHaveAccessTo = HashSet<String>()
    private lateinit var mStorageVolumes: List<StorageVolume>
    private var mHaveAccessToPrimary = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mStorageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
        mStorageVolumes = mStorageManager.storageVolumes

        requestAccessButton.setOnClickListener {
            val primaryVolume = mStorageManager.primaryStorageVolume
            val intent = primaryVolume.createOpenDocumentTreeIntent()
            startActivityForResult(intent, 1)
        }

        releaseAccessButton.setOnClickListener {
            val takeFlags =
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            val uri = buildVolumeUriFromUuid(PRIMARY_UUID)

            contentResolver.releasePersistableUriPermission(uri, takeFlags)
            val toast = Toast.makeText(
                this,
                "Primary volume permission released was released.",
                Toast.LENGTH_SHORT
            )
            toast.setGravity(Gravity.BOTTOM, 0, releaseAccessButton.height)
            toast.show()
            getVolumeStats()
            showVolumeStats()
        }
        getVolumeStats()
        showVolumeStats()

    }

    private fun getVolumeStats() {
        val persistedUriPermissions = contentResolver.persistedUriPermissions
        mStorageVolumePathsWeHaveAccessTo.clear()
        persistedUriPermissions.forEach {
            mStorageVolumePathsWeHaveAccessTo.add(it.uri.toString())
        }
        mVolumeStats.clear()
        mHaveAccessToPrimary = false
        for (storageVolume in mStorageVolumes) {
            val uuid = if (storageVolume.isPrimary) {
                // Primary storage doesn't get a UUID here.
                PRIMARY_UUID
            } else {
                storageVolume.uuid
            }

            val volumeUri = uuid?.let { buildVolumeUriFromUuid(it) }

            when {
                uuid == null ->
                    Log.d(TAG, "UUID is null for ${storageVolume.getDescription(this)}!")
                mStorageVolumePathsWeHaveAccessTo.contains(volumeUri.toString()) -> {
                    Log.d(TAG, "Have access to $uuid")
                    if (uuid == PRIMARY_UUID) {
                        mHaveAccessToPrimary = true
                    }
                    val uri = buildVolumeUriFromUuid(uuid)
                    val docTreeUri = DocumentsContract.buildDocumentUriUsingTree(
                        uri,
                        DocumentsContract.getTreeDocumentId(uri)
                    )
                    mVolumeStats[docTreeUri] = getFileStats(docTreeUri)
                }
                else -> Log.d(TAG, "Don't have access to $uuid")
            }
        }
    }

    private fun showVolumeStats() {
        val sb = StringBuilder()
        if (mVolumeStats.size == 0) {
            sb.appendln("Nothing to see here...")
        } else {
            sb.appendln("All figures are in 1K blocks.")
            sb.appendln()
        }
        mVolumeStats.forEach {
            val lastSeg = it.key.lastPathSegment
            sb.appendln("Volume: $lastSeg")
            val stats = it.value
            val blockSize = stats.f_bsize
            val totalSpace = stats.f_blocks * blockSize / 1024L
            val freeSpace = stats.f_bfree * blockSize / 1024L
            val usedSpace = totalSpace - freeSpace
            sb.appendln(" Used space: ${usedSpace.nice()}")
            sb.appendln(" Free space: ${freeSpace.nice()}")
            sb.appendln("Total space: ${totalSpace.nice()}")
            sb.appendln("----------------")
        }
        volumeStats.text = sb.toString()
        if (mHaveAccessToPrimary) {
            releaseAccessButton.visibility = View.VISIBLE
            requestAccessButton.visibility = View.GONE
        } else {
            releaseAccessButton.visibility = View.GONE
            requestAccessButton.visibility = View.VISIBLE
        }
    }

    private fun buildVolumeUriFromUuid(uuid: String): Uri {
        return DocumentsContract.buildTreeDocumentUri(
            EXTERNAL_STORAGE_AUTHORITY,
            "$uuid:"
        )
    }

    private fun getFileStats(docTreeUri: Uri): StructStatVfs {
        val pfd = contentResolver.openFileDescriptor(docTreeUri, "r")!!
        return fstatvfs(pfd.fileDescriptor)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        Log.d(TAG, "resultCode:$resultCode")
        val uri = data?.data ?: return
        val takeFlags =
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        contentResolver.takePersistableUriPermission(uri, takeFlags)
        Log.d(TAG, "granted uri: ${uri.path}")
        getVolumeStats()
        showVolumeStats()
    }

    companion object {
        fun Long.nice(fieldLength: Int = 12): String = String.format(Locale.US, "%,${fieldLength}d", this)

        const val EXTERNAL_STORAGE_AUTHORITY = "com.android.externalstorage.documents"
        const val PRIMARY_UUID = "primary"
        const val TAG = "AppLog"
    }
}
Run Code Online (Sandbox Code Playgroud)

activity_main.xml

<LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

    <TextView
            android:id="@+id/volumeStats"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginBottom="16dp"
            android:layout_weight="1"
            android:fontFamily="monospace"
            android:padding="16dp" />

    <Button
            android:id="@+id/requestAccessButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginBottom="16dp"
            android:visibility="gone"
            android:text="Request Access to Primary" />

    <Button
            android:id="@+id/releaseAccessButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginBottom="16dp"
            android:text="Release Access to Primary" />
</LinearLayout>   
Run Code Online (Sandbox Code Playgroud)

  • 该演示活动仅限于新加坡武装部队的范围内。将来,应用程序沙箱之外的所有传统文件级访问(目录路径等)是否会被关闭?同时,我会尽可能使用 SAF,并在 SAF 无法满足请求时退回到目录路径。至于空间,我的经验是,其中一些空间总是通过低级格式化、启动分区、隐藏分区、虚拟机堆等流出。该演示与在 shell 中运行时“df”命令所报告的结果一致。 (2认同)