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:

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:

RawPaths, Echo Build, Running on Android 11 DP2
RawPaths, Echo Build, Running on Android 11 DP2

The “SD card” toolbar button will display a checkable submenu with the available storage volumes:

RawPaths, Echo Build, Showing Storage Volume Submenu
RawPaths, Echo Build, Showing Storage Volume Submenu

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:


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.