From 1c80b25af8c6c6f8cf6c80b96e7fdb10a18efa8a Mon Sep 17 00:00:00 2001 From: Anish Kumar Date: Mon, 2 Feb 2026 15:46:06 +0530 Subject: [PATCH] Android: Fix `Bad file descriptor` in SAF/MediaStore in long term access --- .../godot/io/file/MediaStoreData.kt | 21 +++++++++++++++++-- .../org/godotengine/godot/io/file/SAFData.kt | 21 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/MediaStoreData.kt index 3c22aa3d722..728d2808748 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/MediaStoreData.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/MediaStoreData.kt @@ -37,6 +37,7 @@ import android.database.Cursor import android.net.Uri import android.os.Build import android.os.Environment +import android.os.ParcelFileDescriptor import android.provider.MediaStore import android.util.Log import androidx.annotation.RequiresApi @@ -45,6 +46,7 @@ import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.FileOutputStream +import java.io.IOException import java.nio.channels.FileChannel @@ -53,7 +55,7 @@ import java.nio.channels.FileChannel * under scoped storage via the MediaStore API. */ @RequiresApi(Build.VERSION_CODES.Q) -internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) : +internal class MediaStoreData(context: Context, private val filePath: String, accessFlag: FileAccessFlags) : DataAccess.FileChannelDataAccess(filePath) { private data class DataItem( @@ -248,6 +250,7 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi private val id: Long private val uri: Uri override val fileChannel: FileChannel + private val parcelFileDescriptor: ParcelFileDescriptor init { val contentResolver = context.contentResolver @@ -282,7 +285,7 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi id = dataItem.id uri = dataItem.uri - val parcelFileDescriptor = contentResolver.openFileDescriptor(uri, accessFlag.getMode()) + parcelFileDescriptor = contentResolver.openFileDescriptor(uri, accessFlag.getMode()) ?: throw IllegalStateException("Unable to access file descriptor") fileChannel = if (accessFlag == FileAccessFlags.READ) { FileInputStream(parcelFileDescriptor.fileDescriptor).channel @@ -294,4 +297,18 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi fileChannel.truncate(0) } } + + override fun close() { + try { + fileChannel.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing file $filePath.", e) + } finally { + try { + parcelFileDescriptor.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing ParcelFileDescriptor for $filePath.", e) + } + } + } } diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/SAFData.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/SAFData.kt index 23ca5fcf056..982b806e636 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/SAFData.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/SAFData.kt @@ -32,19 +32,21 @@ package org.godotengine.godot.io.file import android.content.Context import android.net.Uri +import android.os.ParcelFileDescriptor import android.provider.DocumentsContract import android.util.Log import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import java.io.FileInputStream import java.io.FileOutputStream +import java.io.IOException import java.nio.channels.FileChannel /** * Implementation of [DataAccess] which handles file access via a content URI obtained using the Android * Storage Access Framework (SAF). */ -internal class SAFData(context: Context, path: String, accessFlag: FileAccessFlags) : +internal class SAFData(context: Context, private val path: String, accessFlag: FileAccessFlags) : DataAccess.FileChannelDataAccess(path) { companion object { @@ -173,9 +175,10 @@ internal class SAFData(context: Context, path: String, accessFlag: FileAccessFla } override val fileChannel: FileChannel + val parcelFileDescriptor: ParcelFileDescriptor init { val uri = resolvePath(context, path, accessFlag) - val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, accessFlag.getMode()) + parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, accessFlag.getMode()) ?: throw IllegalStateException("Unable to access file descriptor") fileChannel = if (accessFlag == FileAccessFlags.READ) { FileInputStream(parcelFileDescriptor.fileDescriptor).channel @@ -187,4 +190,18 @@ internal class SAFData(context: Context, path: String, accessFlag: FileAccessFla fileChannel.truncate(0) } } + + override fun close() { + try { + fileChannel.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing file $path.", e) + } finally { + try { + parcelFileDescriptor.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing ParcelFileDescriptor for $path.", e) + } + } + } }