Does anyone else have experience with this? Our app prompts users to select the root of a USB drive in order to grant access to it. But for a small minority of users, they get an error like this that prevents them from making any selections in the SAF picker:
This seems like an Android system bug to me. I've considered implementing All Files Access as a workaround for these users, but that feels like a nuclear option.
Here is the code I use for opening the SAF picker:
private fun promptSAFPermissions() {
val storageManager = requireActivity().getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.recentStorageVolumes
// The storageVolumes list returns both USB drives and SD cards, so here we try to distinguish
// between these. We use the first one listed, since it's more likely to be a USB drive.
val usbDriveVolume = storageVolumes.firstOrNull {
!it.isPrimary && it.isRemovable && it.state == Environment.MEDIA_MOUNTED && !isRemovableVolumeAnSdCard(it)
}
val intent = when {
usbDriveVolume != null && usbDriveVolume.state == Environment.MEDIA_MOUNTED -> {
// If a matching volume was found and it's mounted, open SAF picker directly to it.
usbDriveVolume.createOpenDocumentTreeIntent()
}
else -> {
// If not matching volume was found, open SAF picker at root and let user try to choose it manually.
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
}
}
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
requireActivity().startActivityForResult(intent, REQUEST_CODE_SAF_PERMISSION)
}
// Try to determine if volume is an SD card.
// Return true if the given StorageVolume is an SD card. First it checks whether "sdcard" is in the volume description.
// Then it reads the proc/mounts file, which contains mount info for all drives on device.
// A USB drive will show on a line like: /dev/block/vold/public:8,1 on /mnt/media_rw/68C9-D020 type vfat (rw,dirsync,nosuid,nodev,noexec,noatime,uid=1023,gid=1023,fmask=0007,dmask=0007,allow_utime=0020,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro)
// An SD card will show on a line like: /dev/block/vold/public:179,65 on /mnt/media_rw/084E-056D type vfat (rw,dirsync,nosuid,nodev,noexec,noatime,uid=1023,gid=1023,fmask=0007,dmask=0007,allow_utime=0020,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro)
// The /dev/block/vold/public:8 indicates a USB drive at 68C9-D020, while /dev/block/vold/public:179 indicates an SD card at 084E-056D.
fun isRemovableVolumeAnSdCard(volume: StorageVolume): Boolean {
// If we have already determined whether the volume is an SD card, the value was stored in uuidIsAnSdCardMap. So just return the value we already found.
PKDrive.instance.uuidIsAnSdCardMap[volume.uuid]?.let { uuidIsAnSdCard ->
return uuidIsAnSdCard
}
val description = volume.getDescription(App.appContext).trim { it <= ' ' }.replace(" ", "").lowercase(Locale.ROOT)
if (description.contains("sdcard")) return true
if (volume.uuid == null) return false
var bufferedReader: BufferedReader? = null
var inputStream: InputStream? = null
var inputStreamReader: InputStreamReader? = null
var sdCardFound = false
try {
val runtime = Runtime.getRuntime()
val process = runtime.exec("mount")
inputStream = process.inputStream
inputStreamReader = InputStreamReader(inputStream)
bufferedReader = BufferedReader(inputStreamReader)
bufferedReader.forEachLine { line ->
if (line.contains(volume.uuid!!, true) && line.contains("fat") && line.contains("media_rw") && line.contains("/dev/block/vold/public:179")) {
sdCardFound = true
return@forEachLine
}
}
} catch (ignored: Exception) {
} finally {
try {
bufferedReader?.close()
inputStream?.close()
inputStreamReader?.close()
} catch (ignored: Exception) {
}
// Add result of this method to uuidIsAnSdCardMap, so we don't have to repeat this operation for the same StorageVolume in the future.
PKDrive.instance.uuidIsAnSdCardMap[volume.uuid!!] = sdCardFound
}
return sdCardFound
}
