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