Background
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.
The problem
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.
What I've found
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.
The questions
- Is
getAllocatableBytes
indeed the way to get the free space? - How can I get the free and real total space (in some cases I got lower values for some reason) of each StorageVolume, without requesting any permission, just like on Google's app?
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.
Reported by the app using fstatvfs (in 1K blocks):
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
activity_main.xml
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" :
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:
And to show the available and total space, we use StatFs as before:
EDIT: shorter version, without using the real file-path of the storageVolume:
Usage:
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:
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:
Is getAllocatableBytes indeed the way to get the free space?
Android 8.0 Features and APIs states that getAllocatableBytes(UUID):
So, getAllocatableBytes() reports how many bytes could be free for a new file by clearing cache for other apps but may not be currently free. This does not seem to be the right call for a general-purpose file utility.
In any case, getAllocatableBytes(UUID) doesn't seem to work for any volume other than the primary volume due to the inability to get acceptable UUIDs from StorageManager for storage volumes other than the primary volume. See Invalid UUID of storage gained from Android StorageManager? and Bug report #62982912. (Mentioned here for completeness; I realize that you already know about these.) The bug report is now over two years old with no resolution or hint at a work-around, so no love there.
If you want the type of free space reported by "Files by Google" or other file managers, then you will want to approach free space in a different way as explained below.
How can I get the free and real total space (in some cases I got lower values for some reason) of each StorageVolume, without requesting any permission, just like on Google's app?
Here is a procedure to get free and total space for available volumes:
Identify external directories: Use getExternalFilesDirs(null) to discover available external locations. What is returned is a File[]. These are directories that our app is permitted to use.
(N.B. According to the documentation, this call returns what are considered to be stable devices such as SD cards. This does not return attached USB drives.)
Identify storage volumes: For each directory returned above, use StorageManager#getStorageVolume(File) to identify the storage volume that contains the directory. We don't need to identify the top-level directory to get the storage volume, just a file from the storage volume, so these directories will do.
Calculate total and used space: Determine the space on the storage volumes. The primary volume is treated differently from an SD card.
For the primary volume: Using StorageStatsManager#getTotalBytes(UUID get the nominal total bytes of storage on the primary device using StorageManager#UUID_DEFAULT . The value returned treats a kilobyte as 1,000 bytes (rather than 1,024) and a gigabyte as 1,000,000,000 bytes instead of 230. On my SamSung Galaxy S7 the value reported is 32,000,000,000 bytes. On my Pixel 3 emulator running API 29 with 16 MB of storage, the value reported is 16,000,000,000.
Here is the trick: If you want the numbers reported by "Files by Google", use 103 for a kilobyte, 106 for a megabyte and 109 for a gigabyte. For other file managers 210, 220 and 230 is what works. (This is demonstrated below.) See this for more information on these units.
To get free bytes, use StorageStatsManager#getFreeBytes(uuid). Used bytes is the difference between total bytes and free bytes.
For non-primary volumes: Space calculations for non-primary volumes is straightforward: For total space used File#getTotalSpace and File#getFreeSpace for the free space.
Here are a couple of screens shots that display volume stats. The first image shows the output of the StorageVolumeStats app (included below the images) and "Files by Google." The toggle button at the top of the top section switches the app between using 1,000 and 1,024 for kilobytes. As you can see, the figures agree. (This is a screen shot from a device running Oreo. I was unable to get the beta version of "Files by Google" loaded onto an Android Q emulator.)
The following image shows the StorageVolumeStats app at the top and output from "EZ File Explorer" on the bottom. Here 1,024 is used for kilobytes and the two apps agree on the total and free space available except for rounding.
MainActivity.kt
This small app is just the main activity. The manifest is generic, compileSdkVersion and targetSdkVersion are set to 29. minSdkVersion is 26.
Addendum
Let's get more comfortable with using getExternalFilesDirs():
We call Context#getExternalFilesDirs() in the code. Within this method a call is made to Environment#buildExternalStorageAppFilesDirs() which calls Environment#getExternalDirs() to obtain the volume list from StorageManager. This storage list is used to create the paths we see returned from Context#getExternalFilesDirs() by appending some static path segments to the path identified by each storage volume.
We would really want access to Environment#getExternalDirs() so we can immediately determine space utilization, but we are restricted. Since the call we make depends upon a file list that is generated from the volume list, we can be comfortable that all volumes are covered by out code and we can get the space utilization information we need.