Действительно ли 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](https://i.stack.imgur.com/31eTIl.jpg)
На следующем рисунке вверху показано приложение StorageVolumeStats , а в нижней части выводится «EZ File Explorer».Здесь 1024 используется для килобайтов, и два приложения согласовывают общее и доступное свободное пространство, за исключением округления.
![enter image description here](https://i.stack.imgur.com/M1U43l.jpg)
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 () , поэтому мы можем сразу определить использование пространства, но мы ограничены.Поскольку вызов, который мы делаем, зависит от списка файлов, сгенерированного из списка томов, нам может быть удобно, чтобы все тома были покрыты нашим кодом, и мы можем получить необходимую информацию об использовании пространства.