Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,50 @@ import app.gamenative.R
import app.gamenative.utils.CustomGameScanner

/**
* Converts a document tree URI to a file path.
* Returns null if conversion fails.
* Resolves the filesystem root of a storage volume identified by the volume id
* from a document tree URI (e.g. "6A1F-93F0").
*
* SD cards typically live at /storage/<uuid>, but USB OTG drives on some devices
* (e.g. Samsung) are only mounted at the path reported by
* [android.os.storage.StorageVolume.getDirectory] (such as /mnt/media_rw/<uuid>),
* with no /storage view at all.
*
* @param context Used to query [android.os.storage.StorageManager] for mounted volumes.
* @param volumeId The volume id portion of a tree document id (the part before ":").
* @return The volume's mount point. Prefers /storage/<volumeId> when it exists,
* otherwise the directory reported by StorageManager, falling back to
* /storage/<volumeId> when the volume cannot be resolved.
*/
fun getPathFromTreeUri(uri: Uri?): String? {
private fun resolveVolumeRoot(context: Context, volumeId: String): String {
val defaultRoot = "/storage/$volumeId"
if (java.io.File(defaultRoot).exists()) {
return defaultRoot
}
val sm = context.getSystemService(android.os.storage.StorageManager::class.java)
val volume = sm?.storageVolumes?.firstOrNull {
it.uuid?.equals(volumeId, ignoreCase = true) == true
}
val resolved = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
volume?.directory?.absolutePath
} else {
null
}
return resolved ?: defaultRoot
}

/**
* Converts a document tree URI (from e.g. [ActivityResultContracts.OpenDocumentTree])
* to a raw filesystem path.
*
* Handles the primary volume ("primary:" document ids) as well as secondary volumes
* such as SD cards and USB OTG drives, whose mount point is resolved via
* [resolveVolumeRoot].
*
* @param context Used to resolve secondary volume mount points.
* @param uri The tree URI returned by the document picker, or null.
* @return The resolved filesystem path, or null if [uri] is null or conversion fails.
*/
fun getPathFromTreeUri(context: Context, uri: Uri?): String? {
if (uri == null) return null

return try {
Expand All @@ -41,16 +81,12 @@ fun getPathFromTreeUri(uri: Uri?): String? {
if (parts.size == 2) {
val volumeId = parts[0]
val path = parts[1]
val possiblePath = if (path.isEmpty()) {
"/storage/$volumeId"
val volumeRoot = resolveVolumeRoot(context, volumeId)
return if (path.isEmpty()) {
volumeRoot
} else {
"/storage/$volumeId/$path"
}
val file = java.io.File(possiblePath)
if (file.exists() || file.parentFile?.exists() == true) {
return possiblePath
"$volumeRoot/$path"
}
return possiblePath
}
}

Expand Down Expand Up @@ -127,7 +163,7 @@ fun rememberCustomGameFolderPicker(
return@rememberLauncherForActivityResult
}

val path = getPathFromTreeUri(uri)
val path = getPathFromTreeUri(context, uri)
if (path != null) {
onPathSelected(path)
} else {
Expand Down
Loading