Как получить бесплатный и общий размер каждого StorageVolume? - PullRequest
15 голосов
/ 19 июня 2019

Фон

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

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

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

Проблема

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

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

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

Однако каким-то образом приложение Google "Files by 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. * * * * * * * * * * * * * * * * * * * * * * *1062* Действительно, способ получитьсвободное место?
  2. Как я могу получить свободное и реальное общее пространство (в некоторых случаях я получил более низкие значения по какой-то причине) каждого StorageVolume, не запрашивая никакого разрешения, как в приложении Google?

Ответы [ 3 ]

5 голосов
/ 27 июня 2019

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

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

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

"/ 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/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 (в блоках по 1 КБ):

Для / tree / primary: / document / primary: Total =793 488 использованных мест = 647 652 доступных = 129 452

Для / tree / 1D03-2E0E: / document / 1D03-2E0E: Всего = 522 228 использованных мест = 90 доступно = 522 138

Итогоматч.

fstatvfs описано здесь .

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

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

enter image description here

MainActivity.кт

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 голос
/ 21 июня 2019

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

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

Интересно, как различные приложения (например, приложения для управления файлами, такие как 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
}

И чтобы показать доступное и общее пространство, мы используем 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")
}

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

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")
        }
0 голосов
/ 20 июля 2019

Действительно ли 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) , чтобы обнаружить доступные внешние расположения. Возвращается файл [] . Это каталоги, которые нашему приложению разрешено использовать.

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 . Значение возвращенный обрабатывает килобайт как 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 - это то, что работает.(Это показано ниже.) См. this для получения дополнительной информации об этих единицах.

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

Для неосновных томов: Пространственные вычисления для неосновных томов просты: для всего используемого пространства Файл # getTotalSpace и Файл # 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 () , добавляя некоторые сегменты статического пути к пути, определенному каждым томом хранилища.

Нам действительно нужен доступto Environment # getExternalDirs () , поэтому мы можем сразу определить использование пространства, но мы ограничены.Поскольку вызов, который мы делаем, зависит от списка файлов, сгенерированного из списка томов, нам может быть удобно, чтобы все тома были покрыты нашим кодом, и мы можем получить необходимую информацию об использовании пространства.

...