“getAllocatableBytes”确实是获取可用空间的方法吗?
Android 8.0 特性和API中提到了“getAllocatableBytes(UUID)”:
“当你需要为大文件分配磁盘空间时,可以考虑使用新的allocateBytes(FileDescriptor, long) API,它将自动清除属于其他应用程序的缓存文件(根据需要),以满足您的请求。在决定设备是否有足够的磁盘空间来容纳您的新数据时,请调用getAllocatableBytes(UUID)而不是使用getUsableSpace(),因为前者将考虑系统愿意代表您清除的任何缓存数据。”
因此,“getAllocatableBytes()”报告通过清除其他应用程序的缓存可以释放多少字节的空间,但可能当前并不是空闲状态。这似乎不是通用文件实用程序的正确调用。
无论如何,由于无法从StorageManager获取除主要卷以外的存储卷的可接受UUID,因此似乎无法对除主要卷以外的任何卷使用getAllocatableBytes(UUID)。请参见
Invalid UUID of storage gained from Android StorageManager?和
Bug report #62982912。(在这里提及是为了完整性; 我知道您已经知道这些内容。)缺陷报告现在已经超过两年,没有解决方案或工作提示,所以不要期望有帮助。
如果您想要“Files by Google”或其他文件管理器报告的空闲空间类型,则需要以下述方式使用不同方法来处理可用空间。
如何获取每个StorageVolume的免费和实际总空间(在某些情况下,由于某些原因,我得到了较低的值),而不需要请求任何权限,就像在Google的应用程序中一样?
以下是获取可用卷的免费和总空间的过程:
识别外部目录:使用
getExternalFilesDirs(null)查找可用的外部位置。返回的是一个
File[]。这些是我们的应用程序被允许使用的目录。
extDirs = {File
2@9489
0 = {File@9509} "/storage/emulated/0/Android/data/com.example.storagevolumes/files"
1 = {File@9510} "/storage/14E4-120B/Android/data/com.example.storagevolumes/files"}
(注:根据文档,此调用返回被视为稳定设备的内容,例如SD卡。这不会返回已连接的USB驱动器。)
识别存储卷:对于上面返回的每个目录,使用
StorageManager#getStorageVolume(File)来识别包含该目录的存储卷。我们不需要识别顶层目录以获取存储卷,只需要从存储卷中获取一个文件即可,因此这些目录就可以了。
计算总空间和已用空间:确定存储卷上的空间。主卷与SD卡的处理方式不同。
对于主卷:使用StorageStatsManager#getTotalBytes(UUID)使用StorageManager#UUID_DEFAULT获取主设备上存储的名义总字节数。返回的值将千字节视为1,000字节(而不是1,024),将千兆字节视为109字节,而不是230字节。在我的三星Galaxy S7上,报告的值为32,000,000,000字节。在运行API 29且具有16 MB存储空间的Pixel 3模拟器上,报告的值为16,000,000,000。
这里有个诀窍:如果您想要“Files by Google”报告的数字,请使用103作为千字节,106作为兆字节和109作为千兆字节。对于其他文件管理器,210、220和230是有效的。(如下所示)有关这些单位的更多信息,请参见this。
要获取可用的字节,请使用
StorageStatsManager#getFreeBytes(uuid)。已使用的字节是总字节和可用字节之差。
对于非主要卷,非常简单的空间计算:对于已使用的总空间
File#getTotalSpace和可用空间
File#getFreeSpace。
以下是显示音量统计信息的几个屏幕截图。第一张图片显示了
StorageVolumeStats应用程序的输出(包括在图像下面),以及“Files by Google”。顶部部分的切换按钮可以在使用1,000和1,024进行千字节转换之间切换。正如您所看到的,这些数字是一致的。(这是从运行Oreo的设备中截取的屏幕截图。我无法将“Files by Google”的测试版加载到Android Q模拟器上。)
![enter image description here](https://istack.dev59.com/31eTIl.webp)
下图显示了顶部的
StorageVolumeStats应用程序和底部的"EZ File Explorer"输出。这里使用1,024作为千字节,除了四舍五入外,两个应用程序在总可用空间和可用空间方面达成一致。
![enter image description here](https://istack.dev59.com/M1U43l.webp)
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() {
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) {
val uuid = StorageManager.UUID_DEFAULT
val storageStatsManager =
getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
totalSpace = (storageStatsManager.getTotalBytes(uuid) / 1_000_000_000) * gbToUse
usedSpace = totalSpace - storageStatsManager.getFreeBytes(uuid)
} else {
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)
@Suppress("unused")
fun StorageVolume.getStorageVolumePath(): String {
return try {
javaClass
.getMethod("getPath")
.invoke(this) as String
} catch (e: Exception) {
e.printStackTrace()
""
}
}
const val KB = 1_000L
const val MB = KB * KB
const val GB = KB * KB * KB
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(),以便我们可以立即确定空间利用率,但我们受到了限制。由于我们所做的调用取决于从卷列表生成的文件列表,因此我们可以确信所有卷都被我们的代码覆盖,并且我们可以获得所需的空间利用信息。