Merge pull request #113367 from syntaxerror247/persistable_uri_perm

Android: Add method to take persistable URI permission
This commit is contained in:
Thaddeus Crews 2025-12-02 11:52:09 -06:00
commit bed803fcda
No known key found for this signature in database
GPG key ID: 8C6E5FEB5FC03CCC
3 changed files with 37 additions and 18 deletions

View file

@ -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 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). [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. 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]. 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].
Tree URIs should be saved and reused; they remain valid across app restarts as long as the directory is not moved, renamed, or deleted. 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.
</description> </description>
</method> </method>
<method name="file_dialog_with_options_show"> <method name="file_dialog_with_options_show">

View file

@ -85,28 +85,12 @@ internal class FilePicker {
for (i in 0 until clipData.itemCount) { for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri val uri = clipData.getItemAt(i).uri
uri?.let { 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()) selectedFiles.add(it.toString())
} }
} }
} else { } else {
val uri: Uri? = data?.data val uri: Uri? = data?.data
uri?.let { 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()) selectedFiles.add(it.toString())
} }
} }

View file

@ -30,6 +30,10 @@
package org.godotengine.godot.plugin 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.Godot
import org.godotengine.godot.variant.Callable import org.godotengine.godot.variant.Callable
@ -39,6 +43,8 @@ import org.godotengine.godot.variant.Callable
* @see <a href="https://docs.godotengine.org/en/latest/tutorials/platform/android/javaclasswrapper_and_androidruntimeplugin.html">Integrating with Android APIs</a> * @see <a href="https://docs.godotengine.org/en/latest/tutorials/platform/android/javaclasswrapper_and_androidruntimeplugin.html">Integrating with Android APIs</a>
*/ */
class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) { class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) {
private val TAG = AndroidRuntimePlugin::class.java.simpleName
override fun getPluginName() = "AndroidRuntime" override fun getPluginName() = "AndroidRuntime"
/** /**
@ -68,4 +74,24 @@ class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) {
fun createCallableFromGodotCallable(godotCallable: Callable): java.util.concurrent.Callable<Any> { fun createCallableFromGodotCallable(godotCallable: Callable): java.util.concurrent.Callable<Any> {
return java.util.concurrent.Callable { godotCallable.call() } 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
}
} }