Google (sadly) plans to ruin storage permission so that apps won't be able to access the file system using the standard File API (and file-paths). Many are against it as it changes the way apps can access the storage and in many ways it's a restricted and limited API.
As a result, we will need to use SAF (storage access framework) entirely on some future Android version (on Android Q we can, at least temporarily, use a flag to use the normal storage permission), if we wish to deal with various storage volumes and reach all files there.
So, for example, suppose you want to make a file manager and show all the storage volumes of the device, and show for each of them how many total and free bytes there are. Such a thing seems very legitimate, but as I can't find a way to do such a thing.
Starting from API 24 (here), we finally have the ability to list all of the storage volumes, as such:
val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
Thing is, there is no function for each of the items on this list to get its size and free space.
However, somehow, Google's "Files by Google" app manages to get this information without any kind of permission being granted :
And this was tested on Galaxy Note 8 with Android 8. Not even the latest version of Android.
So this means there should be a way to get this information without any permission, even on Android 8.
There is something similar to getting free-space, but I'm not sure if it's indeed that. It seems as such, though. Here's the code for it:
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)}")
}
}
However, I can't find something similar for getting the total space of each of the StorageVolume instances. Assuming I'm correct on this, I've requested it here.
You can find more of what I've found in the answer I wrote to this question, but currently it's all a mix of workarounds and things that aren't workarounds but work in some cases.
getAllocatableBytes
indeed the way to get the free space?The following uses fstatvfs(FileDescriptor)
to retrieve stats without resorting to reflection or traditional file system methods.
To check the output of the program to make sure it is producing reasonable result for total, used and available space I ran the "df" command on an Android Emulator running API 29.
Output of "df" command in adb shell reporting 1K blocks:
"/data" corresponds to the "primary" UUID used when by StorageVolume#isPrimary is true.
"/storage/1D03-2E0E" corresponds to the "1D03-2E0E" UUID reported by 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
Reported by the app using fstatvfs (in 1K blocks):
For /tree/primary:/document/primary: Total=793,488 used space=647,652 available=129,452
For /tree/1D03-2E0E:/document/1D03-2E0E: Total=522,228 used space=90 available=522,138
The totals match.
fstatvfs is described here.
Detail on what fstatvfs returns can be found here.
The following little app displays used, free and total bytes for volumes that are accessible.
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>
Found a workaround, by using what I wrote here , and mapping each StorageVolume with a real file as I wrote here. Sadly this might not work in the future, as it uses a lot of "tricks" :
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")
}
}
Seems to work on both emulator (that has primary storage and SD-card) and real device (Pixel 2), both on Android Q beta 4.
A bit better solution which wouldn't use reflection, could be to put a unique file in each of the paths we get on ContextCompat.getExternalCacheDirs
, and then try to find them via each of the StorageVolume instances. It is tricky though because you don't know when to start the search, so you will need to check various paths till you reach the destination. Not only that, but as I wrote here, I don't think there is an official way to get the Uri or DocumentFile or File or file-path of each StorageVolume.
Anyway, weird thing is that the total space is lower than the real one. Probably as it's a partition of what's the maximum that's really available to the user.
I wonder how come various apps (such as file manager apps, like Total Commander) get the real total device storage.
EDIT: OK got another workaround, which is probably more reliable, based on the storageManager.getStorageVolume(File) function.
So here is the merging of the 2 workarounds:
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
}
And to show the available and total space, we use StatFs as before:
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")
}
EDIT: shorter version, without using the real file-path of the 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
}
Usage:
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")
}
Note that this solution doesn't require any kind of permission.
--
EDIT: I actually found out that I tried to do it in the past, but for some reason it crashed for me on the SD-card StoraveVolume on the emulator:
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")
}
The good news is that for the primary storageVolume, you get the real total space of it.
On a real device it also crashes for the SD-card, but not for the primary one.
So here's the latest solution for this, gathering the above:
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")
}
Updated answer for Android R:
fun getStorageVolumesAccessState(context: Context) {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
val storageStatsManager = context.getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
for (storageVolume in storageVolumes) {
var freeSpace: Long = 0L
var totalSpace: Long = 0L
val path = getPath(context, storageVolume)
if (storageVolume.isPrimary) {
totalSpace = storageStatsManager.getTotalBytes(StorageManager.UUID_DEFAULT)
freeSpace = storageStatsManager.getFreeBytes(StorageManager.UUID_DEFAULT)
} else if (path != null) {
val file = File(path)
freeSpace = file.freeSpace
totalSpace = file.totalSpace
}
val usedSpace = totalSpace - freeSpace
val freeSpaceStr = Formatter.formatFileSize(context, freeSpace)
val totalSpaceStr = Formatter.formatFileSize(context, totalSpace)
val usedSpaceStr = Formatter.formatFileSize(context, usedSpace)
Log.d("AppLog", "${storageVolume.getDescription(context)} - path:$path total:$totalSpaceStr used:$usedSpaceStr free:$freeSpaceStr")
}
}
fun getPath(context: Context, storageVolume: StorageVolume): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
storageVolume.directory?.absolutePath?.let { return it }
try {
return storageVolume.javaClass.getMethod("getPath").invoke(storageVolume) as String
} catch (e: Exception) {
}
try {
return (storageVolume.javaClass.getMethod("getPathFile").invoke(storageVolume) as File).absolutePath
} catch (e: Exception) {
}
val extDirs = context.getExternalFilesDirs(null)
for (extDir in extDirs) {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val fileStorageVolume: StorageVolume = storageManager.getStorageVolume(extDir)
?: continue
if (fileStorageVolume == storageVolume) {
var file = extDir
while (true) {
val parent = file.parentFile ?: return file.absolutePath
val parentStorageVolume = storageManager.getStorageVolume(parent)
?: return file.absolutePath
if (parentStorageVolume != storageVolume)
return file.absolutePath
file = parent
}
}
}
try {
val parcel = Parcel.obtain()
storageVolume.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
parcel.readString()
return parcel.readString()
} catch (e: Exception) {
}
return null
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With