diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index fbf6bb793da..e194462cb4d 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -790,8 +790,17 @@ [b]Note:[/b] On macOS, sandboxed apps will save security-scoped bookmarks to retain access to the opened folders across multiple sessions. Use [method OS.get_granted_permissions] to get a list of saved bookmarks. [b]Note:[/b] On Android, this method uses the Android Storage Access Framework (SAF). The file picker returns a URI instead of a filesystem path. This URI can be passed directly to [FileAccess] to perform read/write operations. - When using [constant FILE_DIALOG_MODE_OPEN_DIR], it returns a tree URI that grants full access to the selected directory. File operations inside this directory can be performed by passing a path in the form [code]treeUri#relative/path/to/file[/code] to [FileAccess]. - Tree URIs should be saved and reused; they remain valid across app restarts as long as the directory is not moved, renamed, or deleted. + When using [constant FILE_DIALOG_MODE_OPEN_DIR], it returns a tree URI that grants full access to the selected directory. File operations inside this directory can be performed by passing a path on the form [code]treeUri#relative/path/to/file[/code] to [FileAccess]. + To avoid opening the file picker again after each app restart, you can take persistable URI permission as follows: + [codeblocks] + [gdscript] + val uri = "content://com.android..." # URI of the selected file or folder. + val persist = true # Set to false to release the persistable permission. + var android_runtime = Engine.get_singleton("AndroidRuntime") + android_runtime.updatePersistableUriPermission(uri, persist) + [/gdscript] + [/codeblocks] + The persistable URI permission remains valid across app restarts as long as the directory is not moved, renamed, or deleted. diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/FilePicker.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/FilePicker.kt index 3c22e38e2a9..cf67f3252db 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/FilePicker.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/FilePicker.kt @@ -85,28 +85,12 @@ internal class FilePicker { for (i in 0 until clipData.itemCount) { val uri = clipData.getItemAt(i).uri uri?.let { - try { - context.contentResolver.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - } catch (e: SecurityException) { - Log.d(TAG, "Unable to persist URI: $it", e) - } selectedFiles.add(it.toString()) } } } else { val uri: Uri? = data?.data uri?.let { - try { - context.contentResolver.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - } catch (e: SecurityException) { - Log.w(TAG, "Unable to persist URI: $it", e) - } selectedFiles.add(it.toString()) } } diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt index ca1c32cce9e..4394d514041 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt @@ -30,6 +30,10 @@ package org.godotengine.godot.plugin +import android.content.Intent +import android.util.Log +import androidx.core.net.toUri + import org.godotengine.godot.Godot import org.godotengine.godot.variant.Callable @@ -39,6 +43,8 @@ import org.godotengine.godot.variant.Callable * @see Integrating with Android APIs */ class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) { + private val TAG = AndroidRuntimePlugin::class.java.simpleName + override fun getPluginName() = "AndroidRuntime" /** @@ -68,4 +74,24 @@ class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) { fun createCallableFromGodotCallable(godotCallable: Callable): java.util.concurrent.Callable { return java.util.concurrent.Callable { godotCallable.call() } } + + /** + * Helper method to take/release persistable URI permission. + */ + @UsedByGodot + fun updatePersistableUriPermission(uriString: String, persist: Boolean): Boolean { + try { + val uri = uriString.toUri() + val contentResolver = context.contentResolver + if (persist) { + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } else { + contentResolver.releasePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + } catch (e: RuntimeException) { + Log.d(TAG, "Error updating persistable permission: ", e) + return false + } + return true + } }