Raw Paths Support
However, even without the opt-out, READ_EXTERNAL_STORAGE
works again, more or less as it did from Android 4.4 through Android 9. If you request it, and the user grants it, you can traverse external storage as you were used to.
However, there are major caveats:
- You still do not have access to
Android/
and its subdirectories. We will see this limitation again with the Storage Access Framework. Google now considers those per-app external storage directories to be private. - You still do not have read access to certain other locations, such as
Documents/
andDownloads/
. Basically, if it is controlled byMediaStore
, and you lack read access throughMediaStore
, you also lack read access through raw paths. - Removable storage does not appear to be supported by this “raw paths” feature.
- The documentation mentions reduced performance. This does not appear to be severe, but it may pose issues for performance-sensitive apps.
Note that while the documentation emphasizes native libraries, read access works fine from Java/Kotlin.
Also, methods like getExternalStorageDirectory()
and getExternalStoragePublicDirectory()
on Environment
are still deprecated. Instead, we are supposed to use getDirectory()
on StorageVolume
, which is new to Android 11. As the names suggest, this gives us the root directory for a particular storage volume, whether that is external storage or some removable storage device.
The RawPaths
sample module in the book’s sample project is designed to demonstrate the behavior of READ_EXTERNAL_STORAGE
across a variety of build scenarios. There are five product flavors, with varying configurations:
Flavor | targetSdkVersion |
requestLegacyExternalStorage |
---|---|---|
alfa |
28 | true |
bravo |
29 | true |
charlie |
29 | false |
delta |
30 | true |
echo |
30 | false |
The UI is a crude file explorer. It shows you a list of files and directories for a particular location, starting with some root:
The “SD card” toolbar button will display a checkable submenu with the available storage volumes:
If you switch to a different storage volume, that volume’s root directory will be loaded into the list.
Tapping on a file, by default, will bring up a Toast
showing the CRC32 checksum of the file, used to prove that we have read access to the file’s contents. Tapping on a directory will load that directory’s contents into the list. However, this is a fairly simplistic file explorer, so there is no way to traverse up the directory tree to get back to a root.
Before loading any of this content, though, MainActivity
will request the READ_EXTERNAL_STORAGE
permission.
Our viewmodel, MainMotor
, gets the roster of StorageVolume
objects from the StorageManager
system service:
val volumes: List<StorageVolume> =
context.getSystemService(StorageManager::class.java)!!.storageVolumes
MainActivity
uses that list to build up the submenu contents. If the user taps on one, MainActivity
calls a loadRoot()
function on MainMotor
. If the app is running on Android 11, that in turn gets the selected StorageVolume
out of that list and retrieves its directory. On older devices, we just use the deprecated Environment.getExternalStorageDirectory()
option instead:
fun loadRoot(volumeIndex: Int = 0) {
if (Build.VERSION.SDK_INT < 30) {
load(Environment.getExternalStorageDirectory())
} else {
load(volumes[volumeIndex].directory!!)
}
}
That directory is then used by the load()
function to get the directory’s contents and calculate the CRC32 checksums for all files in the directory:
fun load(dir: File) {
_states.postValue(MainViewState.Loading)
viewModelScope.launch(Dispatchers.IO) {
try {
val items = dir.listFiles().orEmpty()
.sortedBy { it.name }
.map { file ->
if (file.isDirectory) {
FileItem(file, isDirectory = true)
} else {
FileItem(file,
crc32 = CRC32().let { crc ->
crc.update(file.readBytes())
crc.value
})
}
}
_states.postValue(MainViewState.Content(items))
} catch (t: Throwable) {
Log.e("RawPaths", "Exception loading $dir", t)
_states.postValue(MainViewState.Error)
}
}
}
What you will find is:
- On Android 11, both
delta
andecho
behave the same.delta
requests the scoped storage opt-out (android:requestLegacyExternalStorage="true"
), whileecho
does not. Yet, withREAD_EXTERNAL_STORAGE
, we can still access the contents of external storage, though apparently not removable storage and the other restricted locations noted above. - On Android 10,
android:requestLegacyExternalStorage
has a clear effect. If you use it (bravo
),READ_EXTERNAL_STORAGE
works, even though we havetargetSdkVersion
set to 29. If you do not use it (charlie
), even if the user grantsREAD_EXTERNAL_STORAGE
, you do not have filesystem access to external storage. - On Android 9, before all of these changes took effect,
READ_EXTERNAL_STORAGE
works normally.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.