Merge pull request #98350 from syntaxerror247/android-native-filepicker

[Android] Implement native file picker support
This commit is contained in:
Thaddeus Crews 2024-10-31 20:14:41 -05:00
commit be70c2f873
No known key found for this signature in database
GPG Key ID: 62181B86FE9E5D84
22 changed files with 356 additions and 16 deletions

View File

@ -139,11 +139,12 @@
<description> <description>
Displays OS native dialog for selecting files or directories in the file system. Displays OS native dialog for selecting files or directories in the file system.
Each filter string in the [param filters] array should be formatted like this: [code]*.txt,*.doc;Text Files[/code]. The description text of the filter is optional and can be omitted. See also [member FileDialog.filters]. Each filter string in the [param filters] array should be formatted like this: [code]*.txt,*.doc;Text Files[/code]. The description text of the filter is optional and can be omitted. See also [member FileDialog.filters].
Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int[/code]. Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int[/code]. [b]On Android,[/b] callback argument [code]selected_filter_index[/code] is always zero.
[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG_FILE] feature. Supported platforms include Linux (X11/Wayland), Windows, and macOS. [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG_FILE] feature. Supported platforms include Linux (X11/Wayland), Windows, macOS, and Android.
[b]Note:[/b] [param current_directory] might be ignored. [b]Note:[/b] [param current_directory] might be ignored.
[b]Note:[/b] On Linux, [param show_hidden] is ignored. [b]Note:[/b] On Android, the filter strings in the [param filters] array should be specified using MIME types, for example:[code]image/png, image/jpeg"[/code]. Additionally, the [param mode] [constant FILE_DIALOG_MODE_OPEN_ANY] is not supported on Android.
[b]Note:[/b] On macOS, native file dialogs have no title. [b]Note:[/b] On Android and Linux, [param show_hidden] is ignored.
[b]Note:[/b] On Android and macOS, native file dialogs have no title.
[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.
</description> </description>
</method> </method>
@ -166,7 +167,7 @@
- [code]"values"[/code] - [PackedStringArray] of values. If empty, boolean option (check box) is used. - [code]"values"[/code] - [PackedStringArray] of values. If empty, boolean option (check box) is used.
- [code]"default"[/code] - default selected option index ([int]) or default boolean value ([bool]). - [code]"default"[/code] - default selected option index ([int]) or default boolean value ([bool]).
Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int, selected_option: Dictionary[/code]. Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int, selected_option: Dictionary[/code].
[b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG_FILE] feature. Supported platforms include Linux (X11/Wayland), Windows, and macOS. [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG_FILE_EXTRA] feature. Supported platforms include Linux (X11/Wayland), Windows, and macOS.
[b]Note:[/b] [param current_directory] might be ignored. [b]Note:[/b] [param current_directory] might be ignored.
[b]Note:[/b] On Linux (X11), [param show_hidden] is ignored. [b]Note:[/b] On Linux (X11), [param show_hidden] is ignored.
[b]Note:[/b] On macOS, native file dialogs have no title. [b]Note:[/b] On macOS, native file dialogs have no title.
@ -1889,7 +1890,10 @@
Display server supports spawning text input dialogs using the operating system's native look-and-feel. See [method dialog_input_text]. [b]Windows, macOS[/b] Display server supports spawning text input dialogs using the operating system's native look-and-feel. See [method dialog_input_text]. [b]Windows, macOS[/b]
</constant> </constant>
<constant name="FEATURE_NATIVE_DIALOG_FILE" value="25" enum="Feature"> <constant name="FEATURE_NATIVE_DIALOG_FILE" value="25" enum="Feature">
Display server supports spawning dialogs for selecting files or directories using the operating system's native look-and-feel. See [method file_dialog_show] and [method file_dialog_with_options_show]. [b]Windows, macOS, Linux (X11/Wayland)[/b] Display server supports spawning dialogs for selecting files or directories using the operating system's native look-and-feel. See [method file_dialog_show]. [b]Windows, macOS, Linux (X11/Wayland), Android[/b]
</constant>
<constant name="FEATURE_NATIVE_DIALOG_FILE_EXTRA" value="26" enum="Feature">
The display server supports all features of [constant FEATURE_NATIVE_DIALOG_FILE], with the added functionality of Options and native dialog file access to [code]res://[/code] and [code]user://[/code] paths. See [method file_dialog_show] and [method file_dialog_with_options_show]. [b]Windows, macOS, Linux (X11/Wayland)[/b]
</constant> </constant>
<constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode"> <constant name="MOUSE_MODE_VISIBLE" value="0" enum="MouseMode">
Makes the mouse cursor visible if it is hidden. Makes the mouse cursor visible if it is hidden.

View File

@ -146,6 +146,7 @@
</member> </member>
<member name="filters" type="PackedStringArray" setter="set_filters" getter="get_filters" default="PackedStringArray()"> <member name="filters" type="PackedStringArray" setter="set_filters" getter="get_filters" default="PackedStringArray()">
The available file type filters. Each filter string in the array should be formatted like this: [code]*.txt,*.doc;Text Files[/code]. The description text of the filter is optional and can be omitted. The available file type filters. Each filter string in the array should be formatted like this: [code]*.txt,*.doc;Text Files[/code]. The description text of the filter is optional and can be omitted.
[b]Note:[/b] For android native dialog, MIME types are used like this: [code]image/*, application/pdf[/code].
</member> </member>
<member name="mode_overrides_title" type="bool" setter="set_mode_overrides_title" getter="is_mode_overriding_title" default="true"> <member name="mode_overrides_title" type="bool" setter="set_mode_overrides_title" getter="is_mode_overriding_title" default="true">
If [code]true[/code], changing the [member file_mode] property will set the window title accordingly (e.g. setting [member file_mode] to [constant FILE_MODE_OPEN_FILE] will change the window title to "Open a File"). If [code]true[/code], changing the [member file_mode] property will set the window title accordingly (e.g. setting [member file_mode] to [constant FILE_MODE_OPEN_FILE] will change the window title to "Open a File").
@ -159,12 +160,13 @@
</member> </member>
<member name="show_hidden_files" type="bool" setter="set_show_hidden_files" getter="is_showing_hidden_files" default="false"> <member name="show_hidden_files" type="bool" setter="set_show_hidden_files" getter="is_showing_hidden_files" default="false">
If [code]true[/code], the dialog will show hidden files. If [code]true[/code], the dialog will show hidden files.
[b]Note:[/b] This property is ignored by native file dialogs on Linux. [b]Note:[/b] This property is ignored by native file dialogs on Android and Linux.
</member> </member>
<member name="size" type="Vector2i" setter="set_size" getter="get_size" overrides="Window" default="Vector2i(640, 360)" /> <member name="size" type="Vector2i" setter="set_size" getter="get_size" overrides="Window" default="Vector2i(640, 360)" />
<member name="title" type="String" setter="set_title" getter="get_title" overrides="Window" default="&quot;Save a File&quot;" /> <member name="title" type="String" setter="set_title" getter="get_title" overrides="Window" default="&quot;Save a File&quot;" />
<member name="use_native_dialog" type="bool" setter="set_use_native_dialog" getter="get_use_native_dialog" default="false"> <member name="use_native_dialog" type="bool" setter="set_use_native_dialog" getter="get_use_native_dialog" default="false">
If [code]true[/code], and if supported by the current [DisplayServer], OS native dialog will be used instead of custom one. If [code]true[/code], and if supported by the current [DisplayServer], OS native dialog will be used instead of custom one.
[b]Note:[/b] On Android, it is only supported when using [constant ACCESS_FILESYSTEM]. For access mode [constant ACCESS_RESOURCES] and [constant ACCESS_USERDATA], the system will fall back to custom FileDialog.
[b]Note:[/b] On Linux and macOS, sandboxed apps always use native dialogs to access the host file system. [b]Note:[/b] On Linux and macOS, sandboxed apps always use native dialogs to access the host file system.
[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] Native dialogs are isolated from the base process, file dialog properties can't be modified once the dialog is shown. [b]Note:[/b] Native dialogs are isolated from the base process, file dialog properties can't be modified once the dialog is shown.

View File

@ -72,7 +72,8 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const {
//case FEATURE_MOUSE_WARP: //case FEATURE_MOUSE_WARP:
//case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_DIALOG:
case FEATURE_NATIVE_DIALOG_INPUT: case FEATURE_NATIVE_DIALOG_INPUT:
//case FEATURE_NATIVE_DIALOG_FILE: case FEATURE_NATIVE_DIALOG_FILE:
//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
//case FEATURE_NATIVE_ICON: //case FEATURE_NATIVE_ICON:
//case FEATURE_WINDOW_TRANSPARENCY: //case FEATURE_WINDOW_TRANSPARENCY:
case FEATURE_CLIPBOARD: case FEATURE_CLIPBOARD:
@ -189,6 +190,19 @@ void DisplayServerAndroid::emit_input_dialog_callback(String p_text) {
} }
} }
Error DisplayServerAndroid::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java();
ERR_FAIL_NULL_V(godot_java, FAILED);
file_picker_callback = p_callback;
return godot_java->show_file_picker(p_current_directory, p_filename, p_mode, p_filters);
}
void DisplayServerAndroid::emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths) {
if (file_picker_callback.is_valid()) {
file_picker_callback.call_deferred(p_ok, p_selected_paths, 0);
}
}
TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts() const { TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts() const {
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
ERR_FAIL_NULL_V(godot_io_java, Array()); ERR_FAIL_NULL_V(godot_io_java, Array());

View File

@ -88,6 +88,7 @@ class DisplayServerAndroid : public DisplayServer {
Callable system_theme_changed; Callable system_theme_changed;
Callable input_dialog_callback; Callable input_dialog_callback;
Callable file_picker_callback;
void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const; void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const;
@ -121,6 +122,9 @@ public:
virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override; virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override;
void emit_input_dialog_callback(String p_text); void emit_input_dialog_callback(String p_text);
virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, const FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;
void emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths);
virtual TypedArray<Rect2> get_display_cutouts() const override; virtual TypedArray<Rect2> get_display_cutouts() const override;
virtual Rect2i get_display_safe_area() const override; virtual Rect2i get_display_safe_area() const override;

View File

@ -57,6 +57,7 @@ import com.google.android.vending.expansion.downloader.*
import org.godotengine.godot.error.Error import org.godotengine.godot.error.Error
import org.godotengine.godot.input.GodotEditText import org.godotengine.godot.input.GodotEditText
import org.godotengine.godot.input.GodotInputHandler import org.godotengine.godot.input.GodotInputHandler
import org.godotengine.godot.io.FilePicker
import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.directory.DirectoryAccessHandler
import org.godotengine.godot.io.file.FileAccessHandler import org.godotengine.godot.io.file.FileAccessHandler
import org.godotengine.godot.plugin.AndroidRuntimePlugin import org.godotengine.godot.plugin.AndroidRuntimePlugin
@ -677,6 +678,9 @@ class Godot(private val context: Context) {
for (plugin in pluginRegistry.allPlugins) { for (plugin in pluginRegistry.allPlugins) {
plugin.onMainActivityResult(requestCode, resultCode, data) plugin.onMainActivityResult(requestCode, resultCode, data)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FilePicker.handleActivityResult(context, requestCode, resultCode, data)
}
} }
/** /**
@ -890,6 +894,13 @@ class Godot(private val context: Context) {
mClipboard.setPrimaryClip(clip) mClipboard.setPrimaryClip(clip)
} }
@Keep
private fun showFilePicker(currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FilePicker.showFilePicker(context, getActivity(), currentDirectory, filename, fileMode, filters)
}
}
/** /**
* Popup a dialog to input text. * Popup a dialog to input text.
*/ */

View File

@ -229,6 +229,11 @@ public class GodotLib {
*/ */
public static native void inputDialogCallback(String p_text); public static native void inputDialogCallback(String p_text);
/**
* Invoked on the file picker closed.
*/
public static native void filePickerCallback(boolean p_ok, String[] p_selected_paths);
/** /**
* Invoked on the GL thread to configure the height of the virtual keyboard. * Invoked on the GL thread to configure the height of the virtual keyboard.
*/ */

View File

@ -0,0 +1,160 @@
/**************************************************************************/
/* FilePicker.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
package org.godotengine.godot.io
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import android.util.Log
import androidx.annotation.RequiresApi
import org.godotengine.godot.GodotLib
import org.godotengine.godot.io.file.MediaStoreData
/**
* Utility class for managing file selection and file picker activities.
*
* It provides methods to launch a file picker and handle the result, supporting various file modes,
* including opening files, directories, and saving files.
*/
internal class FilePicker {
companion object {
private const val FILE_PICKER_REQUEST = 1000
private val TAG = FilePicker::class.java.simpleName
// Constants for fileMode values
private const val FILE_MODE_OPEN_FILE = 0
private const val FILE_MODE_OPEN_FILES = 1
private const val FILE_MODE_OPEN_DIR = 2
private const val FILE_MODE_OPEN_ANY = 3
private const val FILE_MODE_SAVE_FILE = 4
/**
* Handles the result from a file picker activity and processes the selected file(s) or directory.
*
* @param context The context from which the file picker was launched.
* @param requestCode The request code used when starting the file picker activity.
* @param resultCode The result code returned by the activity.
* @param data The intent data containing the selected file(s) or directory.
*/
@RequiresApi(Build.VERSION_CODES.Q)
fun handleActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == FILE_PICKER_REQUEST) {
if (resultCode == Activity.RESULT_CANCELED) {
Log.d(TAG, "File picker canceled")
GodotLib.filePickerCallback(false, emptyArray())
return
}
if (resultCode == Activity.RESULT_OK) {
val selectedPaths: MutableList<String> = mutableListOf()
// Handle multiple file selection.
val clipData = data?.clipData
if (clipData != null) {
for (i in 0 until clipData.itemCount) {
val uri = clipData.getItemAt(i).uri
uri?.let {
val filepath = MediaStoreData.getFilePathFromUri(context, uri)
if (filepath != null) {
selectedPaths.add(filepath)
} else {
Log.d(TAG, "null filepath URI: $it")
}
}
}
} else {
val uri: Uri? = data?.data
uri?.let {
val filepath = MediaStoreData.getFilePathFromUri(context, uri)
if (filepath != null) {
selectedPaths.add(filepath)
} else {
Log.d(TAG, "null filepath URI: $it")
}
}
}
if (selectedPaths.isNotEmpty()) {
GodotLib.filePickerCallback(true, selectedPaths.toTypedArray())
} else {
GodotLib.filePickerCallback(false, emptyArray())
}
}
}
}
/**
* Launches a file picker activity with specified settings based on the mode, initial directory,
* file type filters, and other parameters.
*
* @param context The context from which to start the file picker.
* @param activity The activity instance used to initiate the picker. Required for activity results.
* @param currentDirectory The directory path to start the file picker in.
* @param filename The name of the file when using save mode.
* @param fileMode The mode to operate in, specifying open, save, or directory select.
* @param filters Array of MIME types to filter file selection.
*/
@RequiresApi(Build.VERSION_CODES.Q)
fun showFilePicker(context: Context, activity: Activity?, currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) {
val intent = when (fileMode) {
FILE_MODE_OPEN_DIR -> Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
FILE_MODE_SAVE_FILE -> Intent(Intent.ACTION_CREATE_DOCUMENT)
else -> Intent(Intent.ACTION_OPEN_DOCUMENT)
}
val initialDirectory = MediaStoreData.getUriFromDirectoryPath(context, currentDirectory)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && initialDirectory != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirectory)
} else {
Log.d(TAG, "Error cannot set initial directory")
}
if (fileMode == FILE_MODE_OPEN_FILES) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) // Set multi select for FILE_MODE_OPEN_FILES
} else if (fileMode == FILE_MODE_SAVE_FILE) {
intent.putExtra(Intent.EXTRA_TITLE, filename) // Set filename for FILE_MODE_SAVE_FILE
}
// ACTION_OPEN_DOCUMENT_TREE does not support intent type
if (fileMode != FILE_MODE_OPEN_DIR) {
intent.type = "*/*"
if (filters.isNotEmpty()) {
if (filters.size == 1) {
intent.type = filters[0]
} else {
intent.putExtra(Intent.EXTRA_MIME_TYPES, filters)
}
}
intent.addCategory(Intent.CATEGORY_OPENABLE)
}
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)
activity?.startActivityForResult(intent, FILE_PICKER_REQUEST)
}
}
}

View File

@ -38,6 +38,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import java.io.File import java.io.File
@ -46,6 +47,7 @@ import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
/** /**
* Implementation of [DataAccess] which handles access and interactions with file and data * Implementation of [DataAccess] which handles access and interactions with file and data
* under scoped storage via the MediaStore API. * under scoped storage via the MediaStore API.
@ -81,6 +83,10 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi
private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " + private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " +
" AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?" " AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?"
private const val AUTHORITY_MEDIA_DOCUMENTS = "com.android.providers.media.documents"
private const val AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS = "com.android.externalstorage.documents"
private const val AUTHORITY_DOWNLOADS_DOCUMENTS = "com.android.providers.downloads.documents"
private fun getSelectionByPathArguments(path: String): Array<String> { private fun getSelectionByPathArguments(path: String): Array<String> {
return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path)) return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path))
} }
@ -230,6 +236,72 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi
) )
return updated > 0 return updated > 0
} }
fun getUriFromDirectoryPath(context: Context, directoryPath: String): Uri? {
if (!directoryExists(directoryPath)) {
return null
}
// Check if the path is under external storage.
val externalStorageRoot = Environment.getExternalStorageDirectory().absolutePath
if (directoryPath.startsWith(externalStorageRoot)) {
val relativePath = directoryPath.replaceFirst(externalStorageRoot, "").trim('/')
val uri = Uri.Builder()
.scheme("content")
.authority(AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS)
.appendPath("document")
.appendPath("primary:$relativePath")
.build()
return uri
}
return null
}
fun getFilePathFromUri(context: Context, uri: Uri): String? {
// Converts content uri to filepath.
val id = getIdFromUri(uri) ?: return null
if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS) {
val split = id.split(":")
val fileName = split.last()
val relativePath = split.dropLast(1).joinToString("/")
val fullPath = File(Environment.getExternalStorageDirectory(), "$relativePath/$fileName").absolutePath
return fullPath
} else {
val id = id.toLongOrNull() ?: return null
val dataItems = queryById(context, id)
return if (dataItems.isNotEmpty()) {
val dataItem = dataItems[0]
File(Environment.getExternalStorageDirectory(), File(dataItem.relativePath, dataItem.displayName).toString()).absolutePath
} else {
null
}
}
}
private fun getIdFromUri(uri: Uri): String? {
return try {
if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS || uri.authority == AUTHORITY_MEDIA_DOCUMENTS || uri.authority == AUTHORITY_DOWNLOADS_DOCUMENTS) {
val documentId = uri.lastPathSegment ?: throw IllegalArgumentException("Invalid URI: $uri")
documentId.substringAfter(":")
} else {
throw IllegalArgumentException("Unsupported URI format: $uri")
}
} catch (e: Exception) {
Log.d(TAG, "Failed to parse ID from URI: $uri", e)
null
}
}
private fun directoryExists(path: String): Boolean {
return try {
val file = File(path)
file.isDirectory && file.exists()
} catch (e: SecurityException) {
Log.d(TAG, "Failed to check directoryExists: $path", e)
false
}
}
} }
private val id: Long private val id: Long

View File

@ -548,6 +548,22 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_inputDialogCallback(J
} }
} }
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_filePickerCallback(JNIEnv *env, jclass clazz, jboolean p_ok, jobjectArray p_selected_paths) {
DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
if (ds) {
Vector<String> selected_paths;
jint length = env->GetArrayLength(p_selected_paths);
for (jint i = 0; i < length; ++i) {
jstring java_string = (jstring)env->GetObjectArrayElement(p_selected_paths, i);
String path = jstring_to_string(java_string, env);
selected_paths.push_back(path);
env->DeleteLocalRef(java_string);
}
ds->emit_file_picker_callback(p_ok, selected_paths);
}
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) { JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) {
String permission = jstring_to_string(p_permission, env); String permission = jstring_to_string(p_permission, env);
if (permission == "android.permission.RECORD_AUDIO" && p_result) { if (permission == "android.permission.RECORD_AUDIO" && p_result) {

View File

@ -68,6 +68,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_inputDialogCallback(JNIEnv *env, jclass clazz, jstring p_text); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_inputDialogCallback(JNIEnv *env, jclass clazz, jstring p_text);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_filePickerCallback(JNIEnv *env, jclass clazz, jboolean p_ok, jobjectArray p_selected_paths);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz); JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz);

View File

@ -68,6 +68,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_set_clipboard = p_env->GetMethodID(godot_class, "setClipboard", "(Ljava/lang/String;)V"); _set_clipboard = p_env->GetMethodID(godot_class, "setClipboard", "(Ljava/lang/String;)V");
_has_clipboard = p_env->GetMethodID(godot_class, "hasClipboard", "()Z"); _has_clipboard = p_env->GetMethodID(godot_class, "hasClipboard", "()Z");
_show_input_dialog = p_env->GetMethodID(godot_class, "showInputDialog", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); _show_input_dialog = p_env->GetMethodID(godot_class, "showInputDialog", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
_show_file_picker = p_env->GetMethodID(godot_class, "showFilePicker", "(Ljava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)V");
_request_permission = p_env->GetMethodID(godot_class, "requestPermission", "(Ljava/lang/String;)Z"); _request_permission = p_env->GetMethodID(godot_class, "requestPermission", "(Ljava/lang/String;)Z");
_request_permissions = p_env->GetMethodID(godot_class, "requestPermissions", "()Z"); _request_permissions = p_env->GetMethodID(godot_class, "requestPermissions", "()Z");
_get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;");
@ -286,6 +287,29 @@ Error GodotJavaWrapper::show_input_dialog(const String &p_title, const String &p
} }
} }
Error GodotJavaWrapper::show_file_picker(const String &p_current_directory, const String &p_filename, int p_mode, const Vector<String> &p_filters) {
if (_show_file_picker) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED);
jstring j_current_directory = env->NewStringUTF(p_current_directory.utf8().get_data());
jstring j_filename = env->NewStringUTF(p_filename.utf8().get_data());
jint j_mode = p_mode;
jobjectArray j_filters = env->NewObjectArray(p_filters.size(), env->FindClass("java/lang/String"), nullptr);
for (int i = 0; i < p_filters.size(); ++i) {
jstring j_filter = env->NewStringUTF(p_filters[i].utf8().get_data());
env->SetObjectArrayElement(j_filters, i, j_filter);
env->DeleteLocalRef(j_filter);
}
env->CallVoidMethod(godot_instance, _show_file_picker, j_current_directory, j_filename, j_mode, j_filters);
env->DeleteLocalRef(j_current_directory);
env->DeleteLocalRef(j_filename);
env->DeleteLocalRef(j_filters);
return OK;
} else {
return ERR_UNCONFIGURED;
}
}
bool GodotJavaWrapper::request_permission(const String &p_name) { bool GodotJavaWrapper::request_permission(const String &p_name) {
if (_request_permission) { if (_request_permission) {
JNIEnv *env = get_jni_env(); JNIEnv *env = get_jni_env();

View File

@ -59,6 +59,7 @@ private:
jmethodID _set_clipboard = nullptr; jmethodID _set_clipboard = nullptr;
jmethodID _has_clipboard = nullptr; jmethodID _has_clipboard = nullptr;
jmethodID _show_input_dialog = nullptr; jmethodID _show_input_dialog = nullptr;
jmethodID _show_file_picker = nullptr;
jmethodID _request_permission = nullptr; jmethodID _request_permission = nullptr;
jmethodID _request_permissions = nullptr; jmethodID _request_permissions = nullptr;
jmethodID _get_granted_permissions = nullptr; jmethodID _get_granted_permissions = nullptr;
@ -105,6 +106,7 @@ public:
bool has_has_clipboard(); bool has_has_clipboard();
bool has_clipboard(); bool has_clipboard();
Error show_input_dialog(const String &p_title, const String &p_message, const String &p_existing_text); Error show_input_dialog(const String &p_title, const String &p_message, const String &p_existing_text);
Error show_file_picker(const String &p_current_directory, const String &p_filename, int p_mode, const Vector<String> &p_filters);
bool request_permission(const String &p_name); bool request_permission(const String &p_name);
bool request_permissions(); bool request_permissions();
Vector<String> get_granted_permissions() const; Vector<String> get_granted_permissions() const;

View File

@ -370,6 +370,7 @@ bool DisplayServerIOS::has_feature(Feature p_feature) const {
// case FEATURE_NATIVE_DIALOG: // case FEATURE_NATIVE_DIALOG:
// case FEATURE_NATIVE_DIALOG_INPUT: // case FEATURE_NATIVE_DIALOG_INPUT:
// case FEATURE_NATIVE_DIALOG_FILE: // case FEATURE_NATIVE_DIALOG_FILE:
// case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
// case FEATURE_NATIVE_ICON: // case FEATURE_NATIVE_ICON:
// case FEATURE_WINDOW_TRANSPARENCY: // case FEATURE_WINDOW_TRANSPARENCY:
case FEATURE_CLIPBOARD: case FEATURE_CLIPBOARD:

View File

@ -216,7 +216,8 @@ bool DisplayServerWayland::has_feature(Feature p_feature) const {
//case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_DIALOG:
//case FEATURE_NATIVE_DIALOG_INPUT: //case FEATURE_NATIVE_DIALOG_INPUT:
#ifdef DBUS_ENABLED #ifdef DBUS_ENABLED
case FEATURE_NATIVE_DIALOG_FILE: { case FEATURE_NATIVE_DIALOG_FILE:
case FEATURE_NATIVE_DIALOG_FILE_EXTRA: {
return true; return true;
} break; } break;
#endif #endif

View File

@ -129,6 +129,7 @@ bool DisplayServerX11::has_feature(Feature p_feature) const {
case FEATURE_ICON: case FEATURE_ICON:
#ifdef DBUS_ENABLED #ifdef DBUS_ENABLED
case FEATURE_NATIVE_DIALOG_FILE: case FEATURE_NATIVE_DIALOG_FILE:
case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
#endif #endif
//case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_DIALOG:
//case FEATURE_NATIVE_DIALOG_INPUT: //case FEATURE_NATIVE_DIALOG_INPUT:

View File

@ -752,6 +752,7 @@ bool DisplayServerMacOS::has_feature(Feature p_feature) const {
case FEATURE_NATIVE_DIALOG: case FEATURE_NATIVE_DIALOG:
case FEATURE_NATIVE_DIALOG_INPUT: case FEATURE_NATIVE_DIALOG_INPUT:
case FEATURE_NATIVE_DIALOG_FILE: case FEATURE_NATIVE_DIALOG_FILE:
case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
case FEATURE_IME: case FEATURE_IME:
case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_WINDOW_TRANSPARENCY:
case FEATURE_HIDPI: case FEATURE_HIDPI:

View File

@ -1133,6 +1133,7 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const {
//case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_DIALOG:
//case FEATURE_NATIVE_DIALOG_INPUT: //case FEATURE_NATIVE_DIALOG_INPUT:
//case FEATURE_NATIVE_DIALOG_FILE: //case FEATURE_NATIVE_DIALOG_FILE:
//case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
//case FEATURE_NATIVE_ICON: //case FEATURE_NATIVE_ICON:
//case FEATURE_WINDOW_TRANSPARENCY: //case FEATURE_WINDOW_TRANSPARENCY:
//case FEATURE_KEEP_SCREEN_ON: //case FEATURE_KEEP_SCREEN_ON:

View File

@ -129,6 +129,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const {
case FEATURE_NATIVE_DIALOG: case FEATURE_NATIVE_DIALOG:
case FEATURE_NATIVE_DIALOG_INPUT: case FEATURE_NATIVE_DIALOG_INPUT:
case FEATURE_NATIVE_DIALOG_FILE: case FEATURE_NATIVE_DIALOG_FILE:
case FEATURE_NATIVE_DIALOG_FILE_EXTRA:
case FEATURE_SWAP_BUFFERS: case FEATURE_SWAP_BUFFERS:
case FEATURE_KEEP_SCREEN_ON: case FEATURE_KEEP_SCREEN_ON:
case FEATURE_TEXT_TO_SPEECH: case FEATURE_TEXT_TO_SPEECH:

View File

@ -67,7 +67,18 @@ void FileDialog::_native_popup() {
} else if (access == ACCESS_USERDATA) { } else if (access == ACCESS_USERDATA) {
root = OS::get_singleton()->get_user_data_dir(); root = OS::get_singleton()->get_user_data_dir();
} }
DisplayServer::get_singleton()->file_dialog_with_options_show(get_title(), ProjectSettings::get_singleton()->globalize_path(dir->get_text()), root, file->get_text().get_file(), show_hidden_files, DisplayServer::FileDialogMode(mode), filters, _get_options(), callable_mp(this, &FileDialog::_native_dialog_cb)); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE_EXTRA)) {
DisplayServer::get_singleton()->file_dialog_with_options_show(get_title(), ProjectSettings::get_singleton()->globalize_path(dir->get_text()), root, file->get_text().get_file(), show_hidden_files, DisplayServer::FileDialogMode(mode), filters, _get_options(), callable_mp(this, &FileDialog::_native_dialog_cb_with_options));
} else {
DisplayServer::get_singleton()->file_dialog_show(get_title(), ProjectSettings::get_singleton()->globalize_path(dir->get_text()), file->get_text().get_file(), show_hidden_files, DisplayServer::FileDialogMode(mode), filters, callable_mp(this, &FileDialog::_native_dialog_cb));
}
}
bool FileDialog::_can_use_native_popup() {
if (access == ACCESS_RESOURCES || access == ACCESS_USERDATA || options.size() > 0) {
return DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE_EXTRA);
}
return DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE);
} }
void FileDialog::popup(const Rect2i &p_rect) { void FileDialog::popup(const Rect2i &p_rect) {
@ -80,7 +91,7 @@ void FileDialog::popup(const Rect2i &p_rect) {
} }
#endif #endif
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE) && (use_native_dialog || OS::get_singleton()->is_sandboxed())) { if (_can_use_native_popup() && (use_native_dialog || OS::get_singleton()->is_sandboxed())) {
_native_popup(); _native_popup();
} else { } else {
ConfirmationDialog::popup(p_rect); ConfirmationDialog::popup(p_rect);
@ -99,7 +110,7 @@ void FileDialog::set_visible(bool p_visible) {
} }
#endif #endif
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE) && (use_native_dialog || OS::get_singleton()->is_sandboxed())) { if (_can_use_native_popup() && (use_native_dialog || OS::get_singleton()->is_sandboxed())) {
if (p_visible) { if (p_visible) {
_native_popup(); _native_popup();
} }
@ -108,7 +119,11 @@ void FileDialog::set_visible(bool p_visible) {
} }
} }
void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter, const Dictionary &p_selected_options) { void FileDialog::_native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter) {
_native_dialog_cb_with_options(p_ok, p_files, p_filter, Dictionary());
}
void FileDialog::_native_dialog_cb_with_options(bool p_ok, const Vector<String> &p_files, int p_filter, const Dictionary &p_selected_options) {
if (!p_ok) { if (!p_ok) {
file->set_text(""); file->set_text("");
emit_signal(SNAME("canceled")); emit_signal(SNAME("canceled"));
@ -182,7 +197,7 @@ void FileDialog::_notification(int p_what) {
#endif #endif
// Replace the built-in dialog with the native one if it started visible. // Replace the built-in dialog with the native one if it started visible.
if (is_visible() && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE) && (use_native_dialog || OS::get_singleton()->is_sandboxed())) { if (is_visible() && _can_use_native_popup() && (use_native_dialog || OS::get_singleton()->is_sandboxed())) {
ConfirmationDialog::set_visible(false); ConfirmationDialog::set_visible(false);
_native_popup(); _native_popup();
} }
@ -1487,7 +1502,7 @@ void FileDialog::set_use_native_dialog(bool p_native) {
#endif #endif
// Replace the built-in dialog with the native one if it's currently visible. // Replace the built-in dialog with the native one if it's currently visible.
if (is_inside_tree() && is_visible() && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_DIALOG_FILE) && (use_native_dialog || OS::get_singleton()->is_sandboxed())) { if (is_inside_tree() && is_visible() && _can_use_native_popup() && (use_native_dialog || OS::get_singleton()->is_sandboxed())) {
ConfirmationDialog::set_visible(false); ConfirmationDialog::set_visible(false);
_native_popup(); _native_popup();
} }

View File

@ -188,8 +188,10 @@ private:
virtual void shortcut_input(const Ref<InputEvent> &p_event) override; virtual void shortcut_input(const Ref<InputEvent> &p_event) override;
bool _can_use_native_popup();
void _native_popup(); void _native_popup();
void _native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter, const Dictionary &p_selected_options); void _native_dialog_cb(bool p_ok, const Vector<String> &p_files, int p_filter);
void _native_dialog_cb_with_options(bool p_ok, const Vector<String> &p_files, int p_filter, const Dictionary &p_selected_options);
bool _is_open_should_be_disabled(); bool _is_open_should_be_disabled();

View File

@ -1056,6 +1056,7 @@ void DisplayServer::_bind_methods() {
BIND_ENUM_CONSTANT(FEATURE_NATIVE_HELP); BIND_ENUM_CONSTANT(FEATURE_NATIVE_HELP);
BIND_ENUM_CONSTANT(FEATURE_NATIVE_DIALOG_INPUT); BIND_ENUM_CONSTANT(FEATURE_NATIVE_DIALOG_INPUT);
BIND_ENUM_CONSTANT(FEATURE_NATIVE_DIALOG_FILE); BIND_ENUM_CONSTANT(FEATURE_NATIVE_DIALOG_FILE);
BIND_ENUM_CONSTANT(FEATURE_NATIVE_DIALOG_FILE_EXTRA);
BIND_ENUM_CONSTANT(MOUSE_MODE_VISIBLE); BIND_ENUM_CONSTANT(MOUSE_MODE_VISIBLE);
BIND_ENUM_CONSTANT(MOUSE_MODE_HIDDEN); BIND_ENUM_CONSTANT(MOUSE_MODE_HIDDEN);

View File

@ -152,6 +152,7 @@ public:
FEATURE_NATIVE_HELP, FEATURE_NATIVE_HELP,
FEATURE_NATIVE_DIALOG_INPUT, FEATURE_NATIVE_DIALOG_INPUT,
FEATURE_NATIVE_DIALOG_FILE, FEATURE_NATIVE_DIALOG_FILE,
FEATURE_NATIVE_DIALOG_FILE_EXTRA,
}; };
virtual bool has_feature(Feature p_feature) const = 0; virtual bool has_feature(Feature p_feature) const = 0;