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 9df90cd446..151224daad 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.Handler; 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 class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh } } + // 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 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh /* 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 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh 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 class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh /** * 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 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 class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh } } - /* 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("*/*"); - break; - case 1: - intent.setType(mimes.get(0)); - break; - default: - intent.setType("*/*"); - intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.toArray(new String[]{})); + /* 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 SDL_FILEDIALOG_OPENFOLDER: + action = Intent.ACTION_OPEN_DOCUMENT_TREE; + break; + default: + 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 2e1991d4b4..d37c1405d4 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 27289bbba0..ef095fa0f8 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 fa646e763d..ec9d1dc863 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 49b42420e1..2618855c09 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); }