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

时间:2019-06-19 08:47:39

标签: android storage-access-framework

背景

Google(严重)plans to ruin storage permission,这样应用程序将无法使用标准File API(和文件路径)访问文件系统。 against it中的许多功能都是use a flag,因为它改变了应用访问存储的方式,并且在许多方面都是受限制的API。

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

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

问题

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

    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes

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

但是,无论如何,Google的enter image description here应用程序设法在未获得任何许可的情况下获取此信息:

here

而且此功能已在装有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)}")
        }
    }

但是,我找不到类似的东西来获取每个StorageVolume实例的总空间。假设我对此没错,我已经要求Android Handling Bitmaps

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

问题

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

3 个答案:

答案 0 :(得分:5)

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

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

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

“ /数据”对应于StorageVolume#isPrimary为true时使用的“主要” 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

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

  

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

     

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

总计匹配。

fstatvfs 被描述为here

fstatvfs 返回的详细信息可以在here中找到。

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

enter image description here

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"
    }
}

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>   

答案 1 :(得分:1)

找到一种解决方法,方法是使用我写的here,并将每个StorageVolume与我写的here时的真实文件进行映射。可悲的是,这将来可能无法使用,因为它会使用很多“技巧”:

        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")
            }
        }

似乎可以同时在Android Q beta 4上的模拟器(具有主存储和SD卡)和真实设备(像素2)上使用。

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

无论如何,奇怪的是总空间小于实际空间。可能是因为它是用户实际可用的最大值的分区。

我想知道各种应用程序(例如文件管理器应用程序,如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
}

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

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")
}

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

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
}

用法:

        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")
        }

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

-

编辑:我实际上发现我过去曾尝试这样做,但是由于某种原因,它在模拟器上的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")
        }

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

在真实设备上,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")
        }

答案 2 :(得分:0)

getAllocatableBytes是否确实是获取可用空间的方式?

Android 8.0 Features and APIs指出 getAllocatableBytes(UUID)

  

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

因此, getAllocatableBytes()报告通过清除其他应用程序的缓存来为一个新文件释放多少字节,但当前可能不空闲。这似乎不是通用文件实用程序的正确选择。

无论如何,由于无法从 StorageManager 获取可接受的UUID,因此 getAllocatableBytes(UUID)不适用于除主卷之外的任何其他卷。主卷以外的其他存储卷。请参见Invalid UUID of storage gained from Android StorageManager?Bug report #62982912。 (这里提到的是完整性,我意识到您已经知道这些。)该错误报告已经有两年多的历史了,没有任何解决方法或任何变通办法的提示,因此在这里一无所知。

如果您希望“ Google的文件”或其他文件管理器报告的可用空间类型,那么您将需要采用以下说明的另一种方法来处理可用空间。

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

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

标识外部目录: :使用getExternalFilesDirs(null)查找可用的外部位置。返回的是一个 File [] 。这些是我们的应用程序允许使用的目录。

  

extDirs = {文件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字节。在运行API 29和16 MB存储的我的Pixel 3模拟器上,报告的值为16,000,000,000。

这是诀窍:如果要“ Google的文件”报告的数字,请使用10 3 表示千字节,使用10 6 表示兆字节,而10 9 表示千兆字节。对于其他文件管理器2 10 ,2 20 和2 30 是有效的。 (下面对此进行了演示。)有关这些单元的更多信息,请参见this

要获取可用字节,请使用StorageStatsManager#getFreeBytes(uuid)。已用字节是总字节与可用字节之差。

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

以下是显示音量状态的几个屏幕快照。第一张图片显示了 StorageVolumeStats 应用程序的输出(包括在图片下方)和“ Google提供的文件”。顶部顶部的切换按钮将应用程序在1000和1,024(千字节)之间切换。如您所见,这些数字是一致的。 (这是运行Oreo的设备的屏幕截图。我无法将Beta版的“ Google提供的文件”加载到Android Q仿真器上。)

enter image description here

下图显示了顶部的 StorageVolumeStats 应用,底部的“ EZ File Explorer”输出。这里1,024用于千字节,并且两个应用程序在可用空间的总和可用空间上达成了一致,除了四舍五入。

enter image description here

MainActivity.kt

这个小应用程序只是主要活动。清单是通用的, compileSdkVersion targetSdkVersion 设置为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
    )
}

附录

让我们更习惯使用 getExternalFilesDirs()

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

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