Как получить бесплатный и общий размер каждого StorageVolume?

Фон

Google (к сожалению) планирует испортить разрешение на хранение, чтобы приложения не могли получить доступ к файловой системе, используя стандартный File API (и пути к файлам). Многие против этого, поскольку это меняет способ, которым приложения могут получить доступ к хранилищу, и во многих отношениях это ограниченный и ограниченный API.

В результате нам потребуется полностью использовать SAF (инфраструктура доступа к хранилищу) в какой-то будущей версии Android (в Android Q мы можем, по крайней мере временно, использовать флаг, чтобы использовать обычное разрешение хранилища), если мы хотим иметь дело с различными объемы хранения и добраться до всех файлов там.

Так, например, предположим, что вы хотите создать файловый менеджер и показать все тома хранилища устройства и показать для каждого из них, сколько есть общих и свободных байтов. Такая вещь кажется очень законной, но, поскольку я не могу найти способ сделать такую вещь.

Эта проблема

Начиная с API 24 ( здесь), мы наконец-то получили возможность перечислять все тома хранения как таковые:

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

Дело в том, что для каждого элемента в этом списке нет функции, чтобы получить его размер и свободное пространство.

Однако приложение Google "Файлы от Google" каким-то образом получает эту информацию без какого-либо разрешения:

enter image description here

И это было проверено на Galaxy Note 8 с Android 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. Предполагая, что я прав в этом, я просил это здесь.

Вы можете найти больше того, что я нашел в ответе, который я написал на этот вопрос, но в настоящее время все это смесь обходных путей и вещей, которые не являются обходными, но работают в некоторых случаях.

Вопросы

  1. Действительно ли getAllocatableBytes это способ получить свободное место?
  2. Как я могу получить свободное и реальное общее пространство (в некоторых случаях я получил более низкие значения по какой-то причине) каждого StorageVolume, не запрашивая никакого разрешения, как в приложении Google?

Ответ 1

Следующее использует fstatvfs(FileDescriptor) для получения статистики, не прибегая к отражению или традиционным методам файловой системы.

Чтобы проверить вывод программы и убедиться, что она дает приемлемый результат для общего, используемого и доступного пространства, я запустил команду "df" в эмуляторе Android с API 29.

Вывод команды "df" в оболочке adb, сообщающей блоки 1K:

"/data" соответствует "первичному" UUID, используемому, когда StorageVolume # isPrimary имеет значение true.

"/storage/1D03-2E0E" соответствует UUID "1D03-2E0E", сообщаемому StorageVolume # 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/[email protected]
/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 описан здесь.

Подробности о том, что возвращает fstatvfs, можно найти здесь.

В следующем небольшом приложении отображаются используемые, свободные и общие байты для доступных томов.

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>   

Ответ 2

Нашел обходной путь, используя то, что я написал здесь, и сопоставив каждый 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")
            }
        }

Кажется, работает как на эмуляторе (который имеет основное хранилище и SD-карту), так и на реальном устройстве (Pixel 2), как на Android Q beta 4.

Немного лучшим решением, в котором не использовалось бы отражение, могло бы быть размещение уникального файла в каждом из путей, которые мы получаем в ContextCompat.getExternalCacheDirs, а затем попытка найти их через каждый из экземпляров StorageVolume. Это сложно, потому что вы не знаете, когда начинать поиск, поэтому вам нужно будет проверять различные пути, пока не дойдете до места назначения. Не только это, но как я уже писал здесь, я не думаю, что есть официальный способ получить Uri или DocumentFile или файл или файл путь каждого StorageVolume.

Во всяком случае, странно то, что общее пространство ниже, чем реальное. Возможно, так как это раздел того, что максимум, что действительно доступно пользователю.

Интересно, как различные приложения (такие как приложения для управления файлами, такие как Total Commander) получают реальную общую память устройства.


РЕДАКТИРОВАТЬ: ОК получил другой обходной путь, который, вероятно, является более надежным, на основе функции storageManager.getStorageVolume(File).

Итак, вот объединение 2 обходных путей:

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

И чтобы показать доступное и общее пространство, мы используем StatFs, как и раньше:

for (storageVolume in storageVolumes) {
    val storageVolumePath = getStorageVolumePath([email protected], 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")
}

РЕДАКТИРОВАТЬ: более короткая версия, без использования реального пути к файлу хранилища тома:

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 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([email protected], 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")
        }

Хорошая новость заключается в том, что для основного хранилища Volume вы получаете его реальное общее пространство.

На реальном устройстве также происходит сбой для 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([email protected], 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")
        }

Ответ 3

Действительно ли getAllocatableBytes - это способ получить свободное место?

Функции и API-интерфейсы Android 8.0 гласят, что getAllocatableBytes (UUID):

Наконец, когда вам нужно выделить место на диске для больших файлов, рассмотрите возможность использования нового API allocateBytes (FileDescriptor, long), который автоматически очистит кэшированные файлы, принадлежащие другим приложениям (при необходимости) для удовлетворения вашего запроса. При принятии решения, достаточно ли на устройстве дискового пространства для хранения новых данных, вызовите getAllocatableBytes (UUID) вместо использования getUsableSpace(), поскольку первый будет учитывать любые кэшированные данные, которые система желает очистить от вашего имени.

Таким образом, getAllocatableBytes() сообщает, сколько байт может быть свободно для нового файла, очистив кэш для других приложений, но в настоящее время может быть свободным. Похоже, это не правильный вызов для файловой утилиты общего назначения.

В любом случае, getAllocatableBytes (UUID), похоже, не работает ни для какого другого тома, кроме основного, из-за невозможности получить приемлемые UUID из StorageManager для томов хранения, отличных от основного тома. Видите неверный UUID хранилища, полученного от Android StorageManager? и сообщение об ошибке # 62982912. (Упоминается здесь для полноты; я понимаю, что вы уже знаете об этом.) Отчету об ошибках уже более двух лет без разрешения или намека на обходной путь, поэтому никакой любви нет.

Если вы хотите указать тип свободного места, сообщаемый "Files by Google" или другими файловыми менеджерами, то вам нужно подходить к свободному пространству другим способом, как описано ниже.

Как я могу получить свободное и реальное общее пространство (в некоторых случаях я получил более низкие значения по какой-то причине) каждого StorageVolume, не запрашивая никакого разрешения, как в приложении Google?

Вот процедура для получения свободного и общего пространства для доступных томов:

Определите внешние каталоги: используйте getExternalFilesDirs (null) для обнаружения доступных внешних расположений. Возвращается файл []. Это каталоги, которые нашему приложению разрешено использовать.

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

(NB. Согласно документации этот вызов возвращает то, что считается стабильным устройством, таким как SD-карты. Это не возвращает подключенные USB-накопители.)

Определите тома хранения: для каждого каталога, возвращенного выше, используйте StorageManager # getStorageVolume (File), чтобы определить том хранения, в котором находится каталог. Нам не нужно идентифицировать каталог верхнего уровня, чтобы получить том хранилища, просто файл из тома хранилища, так что эти каталоги подойдут.

Рассчитать общее и использованное пространство: определить пространство на томах хранения. Основной том обрабатывается иначе, чем на SD-карте.

Для основного тома: используя StorageStatsManager # getTotalBytes (UUID получает номинальный общий объем байт хранилища на основном устройстве, используя StorageManager # UUID_DEFAULT. Возвращаемое значение обрабатывает килобайт как 1000 байтов (а не 1024) и гигабайт как 1 000 000 000 байтов вместо 2 30. На моем SamSung Galaxy S7 сообщаемое значение составляет 32 000 000 000 байт.На моем эмуляторе Pixel 3, работающем с API 29 с 16 МБ памяти, сообщаемое значение составляет 16 000 000 000.

Вот хитрость: если вам нужны числа, сообщаемые "Files by Google", используйте 10 3 для килобайта, 10 6 для мегабайта и 10 9 для гигабайта. Для других файловых менеджеров 2 10 2 20 и 2 30 это то, что работает. (Это показано ниже.) См. Это для получения дополнительной информации об этих единицах.

Чтобы получить бесплатные байты, используйте StorageStatsManager # getFreeBytes (uuid). Используемые байты - это разница между общим байтом и свободным байтом.

Для неосновных томов: расчеты пространства для неосновных томов просты: для общего пространства используются File # getTotalSpace и File # getFreeSpace для свободного пространства.

Вот несколько снимков экранов, на которых отображается статистика объема. Первое изображение показывает выходные данные приложения StorageVolumeStats (включены под изображениями) и "Файлы от Google". Кнопка переключения в верхней части верхней секции переключает приложение между 1000 и 1024 килобайтами. Как видите, цифры согласны. (Это снимок экрана с устройством под управлением Oreo. Мне не удалось загрузить бета-версию "Files by Google" в эмулятор Android Q.)

enter image description here

На следующем рисунке вверху показано приложение StorageVolumeStats, а снизу выводится "EZ File Explorer". Здесь 1024 используется для килобайтов, и эти два приложения согласовывают общее и доступное свободное место за исключением округления.

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(), чтобы мы могли сразу определить использование пространства, но мы ограничены. Поскольку вызов, который мы делаем, зависит от списка файлов, сгенерированного из списка томов, нам может быть удобно, чтобы все тома были покрыты нашим кодом, и мы можем получить необходимую информацию об использовании пространства.