From 439ffd13eb63e79c94df395effe9909d253074de Mon Sep 17 00:00:00 2001
From: crudelios <[EMAIL REDACTED]>
Date: Wed, 13 May 2026 17:50:42 +0100
Subject: [PATCH] Android: Add support for folder dialogs
---
.../main/java/org/libsdl/app/SDLActivity.java | 105 ++++++++++++++----
include/SDL3/SDL_hints.h | 18 +++
src/core/android/SDL_android.c | 36 +++++-
src/core/android/SDL_android.h | 6 +-
src/dialog/android/SDL_androiddialog.c | 19 +---
5 files changed, 135 insertions(+), 49 deletions(-)
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
index 9df90cd446850..151224daadf74 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
@@ -26,6 +26,7 @@
import android.os.LocaleList;
import android.os.Message;
import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
@@ -744,6 +745,11 @@ public void onBackPressed() {
}
}
+ // File dialog types
+ private static final int SDL_FILEDIALOG_OPENFILE = 0;
+ private static final int SDL_FILEDIALOG_SAVEFILE = 1;
+ private static final int SDL_FILEDIALOG_OPENFOLDER = 2;
+
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@@ -752,7 +758,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
/* This is our file dialog */
String[] filelist = null;
- if (data != null) {
+ if (data != null && resultCode == Activity.RESULT_OK) {
Uri singleFileUri = data.getData();
if (singleFileUri == null) {
@@ -767,6 +773,13 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
filelist[i] = uri;
}
} else {
+ /* If the user selected a directory and the persistent permission hint has been set,
+ make the permission persistable */
+ if (mFileDialogState.type == SDL_FILEDIALOG_OPENFOLDER && mFileDialogState.persistable) {
+ mSingleton.getContentResolver().takePersistableUriPermission(singleFileUri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION |
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ }
/* Only one file is selected. */
filelist = new String[]{singleFileUri.toString()};
}
@@ -2099,19 +2112,16 @@ public static int openFileDescriptor(String uri, String mode) throws Exception {
/**
* This method is called by SDL using JNI.
*/
- public static boolean showFileDialog(String[] filters, boolean allowMultiple, boolean forWrite, int requestCode) {
+ public static boolean showFileDialog(String[] filters, boolean allowMultiple,
+ int type, String initialPath, int requestCode) {
if (mSingleton == null) {
return false;
}
- if (forWrite) {
- allowMultiple = false;
- }
-
- /* Convert string list of extensions to their respective MIME types */
+ /* Convert string list of extensions to their respective MIME types (not needed for folder selection) */
ArrayList<String> mimes = new ArrayList<>();
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
- if (filters != null) {
+ if (filters != null && type != SDL_FILEDIALOG_OPENFOLDER) {
for (String pattern : filters) {
String[] extensions = pattern.split(";");
@@ -2129,40 +2139,89 @@ public static boolean showFileDialog(String[] filters, boolean allowMultiple, bo
}
}
- /* Display the file dialog */
- Intent intent = new Intent(forWrite ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_OPEN_DOCUMENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
- switch (mimes.size()) {
- case 0:
- intent.setType("*/*");
+ /* Handle the initial path, if set */
+ Uri initialPathUri = null;
+
+ if (initialPath != null && !initialPath.isEmpty()) {
+ try {
+ initialPathUri = Uri.parse(initialPath);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to parse initial path URI, ignoring initial path", e);
+ }
+ }
+
+ boolean persistable = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_ALLOW_PERSISTENT_FOLDER_ACCESS", false);
+
+ /* Select the intent based on the type */
+ String action;
+ switch (type) {
+ case SDL_FILEDIALOG_OPENFILE:
+ action = Intent.ACTION_OPEN_DOCUMENT;
+ break;
+ case SDL_FILEDIALOG_SAVEFILE:
+ action = Intent.ACTION_CREATE_DOCUMENT;
+ allowMultiple = false;
break;
- case 1:
- intent.setType(mimes.get(0));
+ case SDL_FILEDIALOG_OPENFOLDER:
+ action = Intent.ACTION_OPEN_DOCUMENT_TREE;
break;
default:
- intent.setType("*/*");
- intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.toArray(new String[]{}));
+ Log.e(TAG, "Unsupported file dialog type: " + type);
+ return false;
}
+ /* Prepare the intent with the proper values */
+ Intent intent = new Intent(action);
+ if (type != SDL_FILEDIALOG_OPENFOLDER) {
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
+ switch (mimes.size()) {
+ case 0:
+ intent.setType("*/*");
+ break;
+ case 1:
+ intent.setType(mimes.get(0));
+ break;
+ default:
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.toArray(new String[]{}));
+ }
+ } else {
+ int intent_flags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+ if (persistable) {
+ intent_flags |= Intent.FLAG_GRANT_PREFIX_URI_PERMISSION |
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
+ }
+ intent.addFlags(intent_flags);
+ }
+
+ if (initialPathUri != null) {
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialPathUri);
+ }
+
+ /* Display the file/folder dialog */
try {
mSingleton.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException e) {
- Log.e(TAG, "Unable to open file dialog.", e);
+ Log.e(TAG, "Unable to open dialog.", e);
return false;
}
/* Save current dialog state */
mFileDialogState = new SDLFileDialogState();
mFileDialogState.requestCode = requestCode;
- mFileDialogState.multipleChoice = allowMultiple;
+ mFileDialogState.type = type;
+ mFileDialogState.persistable = persistable;
+
return true;
}
- /* Internal class used to track active open file dialog */
+ /* Internal class used to track active file dialog */
static class SDLFileDialogState {
int requestCode;
- boolean multipleChoice;
+ int type;
+ boolean persistable;
}
/**
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 2e1991d4b4c76..d37c1405d48bf 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -140,6 +140,24 @@ extern "C" {
*/
#define SDL_HINT_ANDROID_TRAP_BACK_BUTTON "SDL_ANDROID_TRAP_BACK_BUTTON"
+/**
+ * A variable to control whether we allow persistent folder access on Android when using the SDL select folder dialog.
+ *
+ * If set to `1`, the selected folder will be accessible persistently across app launches.
+ * That allows the user to only have to select the directory once, and then the app can access it again in the future
+ * without needing to ask the user to select it again.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Persistent folder access is not allowed. (default)
+ * - "1": Persistent folder access is allowed.
+ *
+ * This hint should be set before the SDL folder selection dialog is shown, and can be changed between dialog invocations.
+ *
+ * \since This hint is available since SDL 3.6.0.
+ */
+#define SDL_HINT_ANDROID_ALLOW_PERSISTENT_FOLDER_ACCESS "SDL_ANDROID_ALLOW_PERSISTENT_FOLDER_ACCESS"
+
/**
* A variable setting the app ID string.
*
diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c
index 27289bbba042d..ef095fa0f87a8 100644
--- a/src/core/android/SDL_android.c
+++ b/src/core/android/SDL_android.c
@@ -688,7 +688,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl
midShowTextInput = (*env)->GetStaticMethodID(env, mActivityClass, "showTextInput", "(IIIII)Z");
midSupportsRelativeMouse = (*env)->GetStaticMethodID(env, mActivityClass, "supportsRelativeMouse", "()Z");
midOpenFileDescriptor = (*env)->GetStaticMethodID(env, mActivityClass, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I");
- midShowFileDialog = (*env)->GetStaticMethodID(env, mActivityClass, "showFileDialog", "([Ljava/lang/String;ZZI)Z");
+ midShowFileDialog = (*env)->GetStaticMethodID(env, mActivityClass, "showFileDialog", "([Ljava/lang/String;ZILjava/lang/String;I)Z");
midGetPreferredLocales = (*env)->GetStaticMethodID(env, mActivityClass, "getPreferredLocales", "()Ljava/lang/String;");
if (!midClipboardGetText ||
@@ -3386,18 +3386,34 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeFileDialog)(
}
}
-bool Android_JNI_OpenFileDialog(
+bool Android_JNI_ShowFileDialog(
SDL_DialogFileCallback callback, void *userdata,
- const SDL_DialogFileFilter *filters, int nfilters, bool forwrite,
- bool multiple)
+ const SDL_DialogFileFilter *filters, int nfilters, SDL_FileDialogType type,
+ bool multiple, const char *initialPath)
{
if (mAndroidFileDialogData.callback != NULL) {
SDL_SetError("Only one file dialog can be run at a time.");
return false;
}
- if (forwrite) {
+ // Setup type
+ int dialogType = 0;
+
+ switch (type) {
+ case SDL_FILEDIALOG_OPENFILE:
+ dialogType = 0;
+ break;
+ case SDL_FILEDIALOG_SAVEFILE:
multiple = false;
+ dialogType = 1;
+ break;
+ case SDL_FILEDIALOG_OPENFOLDER:
+ multiple = false;
+ dialogType = 2;
+ break;
+ default:
+ SDL_SetError("Invalid file dialog type");
+ return false;
}
JNIEnv *env = Android_JNI_GetEnv();
@@ -3417,6 +3433,12 @@ bool Android_JNI_OpenFileDialog(
}
}
+ // Setup initial path
+ jstring initialPathString = NULL;
+ if (initialPath && *initialPath) {
+ initialPathString = (*env)->NewStringUTF(env, initialPath);
+ }
+
// Setup data
static SDL_AtomicInt next_request_code;
mAndroidFileDialogData.request_code = SDL_AddAtomicInt(&next_request_code, 1);
@@ -3425,8 +3447,10 @@ bool Android_JNI_OpenFileDialog(
// Invoke JNI
jboolean success = (*env)->CallStaticBooleanMethod(env, mActivityClass,
- midShowFileDialog, filtersArray, (jboolean) multiple, (jboolean) forwrite, mAndroidFileDialogData.request_code);
+ midShowFileDialog, filtersArray, (jboolean) multiple,
+ dialogType, initialPathString, mAndroidFileDialogData.request_code);
(*env)->DeleteLocalRef(env, filtersArray);
+ (*env)->DeleteLocalRef(env, initialPathString);
if (!success) {
mAndroidFileDialogData.callback = NULL;
SDL_AddAtomicInt(&next_request_code, -1);
diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h
index fa646e763d950..ec9d1dc863d3e 100644
--- a/src/core/android/SDL_android.h
+++ b/src/core/android/SDL_android.h
@@ -154,9 +154,9 @@ bool SDL_IsAndroidTablet(void);
bool SDL_IsAndroidTV(void);
// File Dialogs
-bool Android_JNI_OpenFileDialog(SDL_DialogFileCallback callback, void *userdata,
- const SDL_DialogFileFilter *filters, int nfilters, bool forwrite,
- bool multiple);
+bool Android_JNI_ShowFileDialog(SDL_DialogFileCallback callback, void *userdata,
+ const SDL_DialogFileFilter *filters, int nfilters, SDL_FileDialogType type,
+ bool multiple, const char *initialPath);
// Ends C function definitions when using C++
#ifdef __cplusplus
diff --git a/src/dialog/android/SDL_androiddialog.c b/src/dialog/android/SDL_androiddialog.c
index 49b42420e1801..2618855c0967d 100644
--- a/src/dialog/android/SDL_androiddialog.c
+++ b/src/dialog/android/SDL_androiddialog.c
@@ -28,7 +28,7 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil
SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
bool allow_many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false);
- bool is_save;
+ const char *base_folder = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL);
if (SDL_GetHint(SDL_HINT_FILE_DIALOG_DRIVER) != NULL) {
SDL_SetError("File dialog driver unsupported (don't set SDL_HINT_FILE_DIALOG_DRIVER)");
@@ -36,22 +36,7 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil
return;
}
- switch (type) {
- case SDL_FILEDIALOG_OPENFILE:
- is_save = false;
- break;
-
- case SDL_FILEDIALOG_SAVEFILE:
- is_save = true;
- break;
-
- case SDL_FILEDIALOG_OPENFOLDER:
- SDL_Unsupported();
- callback(userdata, NULL, -1);
- return;
- }
-
- if (!Android_JNI_OpenFileDialog(callback, userdata, filters, nfilters, is_save, allow_many)) {
+ if (!Android_JNI_ShowFileDialog(callback, userdata, filters, nfilters, type, allow_many, base_folder)) {
// SDL_SetError is already called when it fails
callback(userdata, NULL, -1);
}