Google(严重)plans to ruin storage permission,这样应用程序将无法使用标准File API(和文件路径)访问文件系统。 against it中的许多功能都是use a flag,因为它改变了应用访问存储的方式,并且在许多方面都是受限制的API。
因此,如果我们需要在将来的某些Android版本上完全使用SAF(存储访问框架)(在Android Q上,我们至少可以暂时here来使用常规存储权限),希望处理各种存储量并访问那里的所有文件。
因此,举例来说,假设您要创建一个文件管理器并显示设备的所有存储量,并为它们中的每一个显示有多少个总字节和可用字节。这样的事情看起来非常合理,但是由于我找不到找到这种方法的方法。
从API 24("Files by Google")开始,我们终于可以列出所有存储卷,如下所示:
val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
事实是,此列表上的每个项目都没有功能来获取其大小和可用空间。
但是,无论如何,Google的应用程序设法在未获得任何许可的情况下获取此信息:
而且此功能已在装有Android 8的Galaxy Note 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实例的总空间。假设我对此没错,我已经要求Android Handling Bitmaps。
您可以在我写给该问题的答案中找到更多的信息,但是目前,这是所有变通办法的结合,这些工作不是变通办法,而是在某些情况下有效。
getAllocatableBytes
确实是获得可用空间的方式吗?答案 0 :(得分:5)
以下使用fstatvfs(FileDescriptor)
来检索统计信息,而无需求助于反射或传统文件系统方法。
要检查程序的输出以确保其产生合理的总空间,已用空间和可用空间,我在运行API 29的Android仿真器上运行了“ df”命令。
在adb shell中输出“ df”命令并报告1K块:
“ /数据”对应于StorageVolume#isPrimary为true时使用的“主要” UUID。
“ / storage / 1D03-2E0E”对应于StorageVolume#uuid报告的“ 1D03-2E0E” 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 报告(以1K块为单位):
对于/ tree / primary:/ document / primary: 总计= 793,488使用空间= 647,652可用= 129,452
对于/ tree / 1D03-2E0E:/ document / 1D03-2E0E: 总计= 522,228个已用空间= 90个可用空间= 522,138
总计匹配。
fstatvfs 被描述为here。
fstatvfs 返回的详细信息可以在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>
答案 1 :(得分:1)
找到一种解决方法,方法是使用我写的here,并将每个StorageVolume与我写的here时的真实文件进行映射。可悲的是,这将来可能无法使用,因为它会使用很多“技巧”:
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")
}
}
似乎可以同时在Android Q beta 4上的模拟器(具有主存储和SD卡)和真实设备(像素2)上使用。
一个更好的解决方案,它不使用反射,可能是将一个唯一的文件放在我们在ContextCompat.getExternalCacheDirs
上获得的每个路径中,然后尝试通过每个StorageVolume实例找到它们。但是,这很棘手,因为您不知道何时开始搜索,因此您将需要检查各种路径,直到到达目的地为止。不仅如此,而且正如我写的here一样,我认为没有一种正式的方法可以获取每个StorageVolume的Uri或DocumentFile或File或文件路径。
无论如何,奇怪的是总空间小于实际空间。可能是因为它是用户实际可用的最大值的分区。
我想知道各种应用程序(例如文件管理器应用程序,如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
}
为了显示可用空间和总空间,我们像以前一样使用StatF:
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")
}
编辑:较短版本,不使用storageVolume的真实文件路径:
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")
}
答案 2 :(得分:0)
getAllocatableBytes是否确实是获取可用空间的方式?
Android 8.0 Features and APIs指出 getAllocatableBytes(UUID):
最后,当您需要为大文件分配磁盘空间时,请考虑使用新的allocateBytes(FileDescriptor,long)API,该API将自动清除属于其他应用程序的缓存文件(根据需要)以满足您的请求。在确定设备是否有足够的磁盘空间来保存新数据时,请调用getAllocatableBytes(UUID)而不是使用getUsableSpace(),因为前者将考虑系统愿意代表您清除的所有缓存数据。
因此, getAllocatableBytes()报告通过清除其他应用程序的缓存来为一个新文件释放多少字节,但当前可能不空闲。这似乎不是通用文件实用程序的正确选择。
无论如何,由于无法从 StorageManager 获取可接受的UUID,因此 getAllocatableBytes(UUID)不适用于除主卷之外的任何其他卷。主卷以外的其他存储卷。请参见Invalid UUID of storage gained from Android StorageManager?和Bug report #62982912。 (这里提到的是完整性,我意识到您已经知道这些。)该错误报告已经有两年多的历史了,没有任何解决方法或任何变通办法的提示,因此在这里一无所知。
如果您希望“ Google的文件”或其他文件管理器报告的可用空间类型,那么您将需要采用以下说明的另一种方法来处理可用空间。
如何获得每个StorageVolume的可用空间和实际总空间(在某些情况下,由于某些原因我得到的值较低),而无需请求任何许可,就像在Google的应用程序上一样?
以下是获取可用卷的可用空间和总空间的过程:
标识外部目录: :使用getExternalFilesDirs(null)查找可用的外部位置。返回的是一个 File [] 。这些是我们的应用程序允许使用的目录。
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获取主设备上的名义存储总字节数。返回的值将千字节视为1,000字节(而不是1,024),将千兆字节视为1,000,000,000字节,而不是2 30 。在我的三星Galaxy S7上,报告的值为32,000,000,000字节。在运行API 29和16 MB存储的我的Pixel 3模拟器上,报告的值为16,000,000,000。
这是诀窍:如果要“ Google的文件”报告的数字,请使用10 3 表示千字节,使用10 6 表示兆字节,而10 9 表示千兆字节。对于其他文件管理器2 10 ,2 20 和2 30 是有效的。 (下面对此进行了演示。)有关这些单元的更多信息,请参见this。
要获取可用字节,请使用StorageStatsManager#getFreeBytes(uuid)。已用字节是总字节与可用字节之差。
对于非主要卷:非主要卷的空间计算非常简单:对于File#getTotalSpace和File#getFreeSpace用于可用空间的总空间。
以下是显示音量状态的几个屏幕快照。第一张图片显示了 StorageVolumeStats 应用程序的输出(包括在图片下方)和“ Google提供的文件”。顶部顶部的切换按钮将应用程序在1000和1,024(千字节)之间切换。如您所见,这些数字是一致的。 (这是运行Oreo的设备的屏幕截图。我无法将Beta版的“ Google提供的文件”加载到Android Q仿真器上。)
下图显示了顶部的 StorageVolumeStats 应用,底部的“ EZ File Explorer”输出。这里1,024用于千字节,并且两个应用程序在可用空间的总和可用空间上达成了一致,除了四舍五入。
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(),以便我们可以立即确定空间利用率,但受到限制。由于我们进行的调用取决于从卷列表生成的文件列表,因此我们可以放心所有卷都被out代码覆盖,并且可以获得所需的空间利用率信息。