Просмотр исходного кода

Android: Add support for folder dialogs

crudelios 2 дней назад
Родитель
Сommit
439ffd13eb

+ 82 - 23
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<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 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("*/*");
+        /* 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;
     }
 
     /**

+ 18 - 0
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.
  *

+ 30 - 6
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);

+ 3 - 3
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

+ 2 - 17
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);
     }