From 86fada6faab919837d084646a8d7dcd93fd60ffc Mon Sep 17 00:00:00 2001 From: Miku AuahDark Date: Mon, 6 May 2024 12:14:20 +0800 Subject: [PATCH] Android: Implement open and save file dialog. --- Android.mk | 2 + CMakeLists.txt | 5 +- android-project/app/proguard-rules.pro | 2 + .../main/java/org/libsdl/app/SDLActivity.java | 111 +++++++++++++ src/core/android/SDL_android.c | 146 +++++++++++++++++- src/core/android/SDL_android.h | 4 + src/dialog/android/SDL_androiddialog.c | 45 ++++++ 7 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 src/dialog/android/SDL_androiddialog.c diff --git a/Android.mk b/Android.mk index a60a0d3636..3798a2516c 100644 --- a/Android.mk +++ b/Android.mk @@ -30,6 +30,8 @@ LOCAL_SRC_FILES := \ $(wildcard $(LOCAL_PATH)/src/core/*.c) \ $(wildcard $(LOCAL_PATH)/src/core/android/*.c) \ $(wildcard $(LOCAL_PATH)/src/cpuinfo/*.c) \ + $(LOCAL_PATH)/src/dialog/SDL_dialog_utils.c \ + $(LOCAL_PATH)/src/dialog/android/SDL_androiddialog.c \ $(wildcard $(LOCAL_PATH)/src/dynapi/*.c) \ $(wildcard $(LOCAL_PATH)/src/events/*.c) \ $(wildcard $(LOCAL_PATH)/src/file/*.c) \ diff --git a/CMakeLists.txt b/CMakeLists.txt index b8cd37904f..06712aeed6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2874,7 +2874,10 @@ endif() if (SDL_DIALOG) sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/SDL_dialog_utils.c) - if(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU) + if(ANDROID) + sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/android/SDL_androiddialog.c) + set(HAVE_SDL_DIALOG TRUE) + elseif(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU) sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/unix/SDL_unixdialog.c) sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/unix/SDL_portaldialog.c) sdl_sources(${SDL3_SOURCE_DIR}/src/dialog/unix/SDL_zenitydialog.c) diff --git a/android-project/app/proguard-rules.pro b/android-project/app/proguard-rules.pro index b608d09b1a..e717ea0b70 100644 --- a/android-project/app/proguard-rules.pro +++ b/android-project/app/proguard-rules.pro @@ -49,6 +49,8 @@ int showToast(java.lang.String, int, int, int, int); native java.lang.String nativeGetHint(java.lang.String); int openFileDescriptor(java.lang.String, java.lang.String); + boolean showFileDialog(java.lang.String[], boolean, boolean, int); + native void onNativeFileDialog(int, java.lang.String[], int); } -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.HIDDeviceManager { 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 a24ded14cc..f99168d59c 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 @@ -4,6 +4,7 @@ import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.UiModeManager; +import android.content.ActivityNotFoundException; import android.content.ClipboardManager; import android.content.ClipData; import android.content.Context; @@ -39,6 +40,7 @@ import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.webkit.MimeTypeMap; import android.widget.Button; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -46,6 +48,7 @@ import android.widget.TextView; import android.widget.Toast; import java.io.FileNotFoundException; +import java.util.ArrayList; import java.util.Hashtable; import java.util.Locale; @@ -227,6 +230,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh protected static Thread mSDLThread; protected static boolean mSDLMainFinished = false; protected static boolean mActivityCreated = false; + private static SDLFileDialogState mFileDialogState = null; protected static SDLGenericMotionListener_API12 getMotionListener() { if (mMotionListener == null) { @@ -719,6 +723,43 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh } } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (mFileDialogState != null && mFileDialogState.requestCode == requestCode) { + /* This is our file dialog */ + String[] filelist = null; + + if (data != null) { + Uri singleFileUri = data.getData(); + + if (singleFileUri == null) { + /* Use Intent.getClipData to get multiple choices */ + ClipData clipData = data.getClipData(); + assert clipData != null; + + filelist = new String[clipData.getItemCount()]; + + for (int i = 0; i < filelist.length; i++) { + String uri = clipData.getItemAt(i).getUri().toString(); + filelist[i] = uri; + } + } else { + /* Only one file is selected. */ + filelist = new String[]{singleFileUri.toString()}; + } + } else { + /* User cancelled the request. */ + filelist = new String[0]; + } + + // TODO: Detect the file MIME type and pass the filter value accordingly. + SDLActivity.onNativeFileDialog(requestCode, filelist, -1); + mFileDialogState = null; + } + } + // Called by JNI from SDL. public static void manualBackButton() { mSingleton.pressBackButton(); @@ -1021,6 +1062,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh public static native void onNativeDarkModeChanged(boolean enabled); public static native boolean nativeAllowRecreateActivity(); public static native int nativeCheckSDLThreadCounter(); + public static native void onNativeFileDialog(int requestCode, String[] filelist, int filter); /** * This method is called by SDL using JNI. @@ -1957,6 +1999,75 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh return -1; } } + + /** + * This method is called by SDL using JNI. + */ + public static boolean showFileDialog(String[] filters, boolean allowMultiple, boolean forWrite, int requestCode) { + if (mSingleton == null) { + return false; + } + + if (forWrite) { + allowMultiple = false; + } + + /* Convert string list of extensions to their respective MIME types */ + ArrayList mimes = new ArrayList<>(); + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + if (filters != null) { + for (String pattern : filters) { + String[] extensions = pattern.split(";"); + + if (extensions.length == 1 && extensions[0].equals("*")) { + /* Handle "*" special case */ + mimes.add("*/*"); + } else { + for (String ext : extensions) { + String mime = mimeTypeMap.getMimeTypeFromExtension(ext); + if (mime != null) { + mimes.add(mime); + } + } + } + } + } + + /* 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[]{})); + } + + try { + mSingleton.startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Unable to open file dialog.", e); + return false; + } + + /* Save current dialog state */ + mFileDialogState = new SDLFileDialogState(); + mFileDialogState.requestCode = requestCode; + mFileDialogState.multipleChoice = allowMultiple; + return true; + } + + /* Internal class used to track active open file dialog */ + static class SDLFileDialogState { + int requestCode; + boolean multipleChoice; + } } /** diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c index 4929039607..bb6e2a00a6 100644 --- a/src/core/android/SDL_android.c +++ b/src/core/android/SDL_android.c @@ -177,6 +177,10 @@ JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeAllowRecreateActivity)( JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeCheckSDLThreadCounter)( JNIEnv *env, jclass jcls); +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeFileDialog)( + JNIEnv *env, jclass jcls, + jint requestCode, jobjectArray fileList, jint filter); + static JNINativeMethod SDLActivity_tab[] = { { "nativeGetVersion", "()Ljava/lang/String;", SDL_JAVA_INTERFACE(nativeGetVersion) }, { "nativeSetupJNI", "()I", SDL_JAVA_INTERFACE(nativeSetupJNI) }, @@ -211,7 +215,8 @@ static JNINativeMethod SDLActivity_tab[] = { { "nativeAddTouch", "(ILjava/lang/String;)V", SDL_JAVA_INTERFACE(nativeAddTouch) }, { "nativePermissionResult", "(IZ)V", SDL_JAVA_INTERFACE(nativePermissionResult) }, { "nativeAllowRecreateActivity", "()Z", SDL_JAVA_INTERFACE(nativeAllowRecreateActivity) }, - { "nativeCheckSDLThreadCounter", "()I", SDL_JAVA_INTERFACE(nativeCheckSDLThreadCounter) } + { "nativeCheckSDLThreadCounter", "()I", SDL_JAVA_INTERFACE(nativeCheckSDLThreadCounter) }, + { "onNativeFileDialog", "(I[Ljava/lang/String;I)V", SDL_JAVA_INTERFACE(onNativeFileDialog) } }; /* Java class SDLInputConnection */ @@ -346,6 +351,7 @@ static jmethodID midShouldMinimizeOnFocusLoss; static jmethodID midShowTextInput; static jmethodID midSupportsRelativeMouse; static jmethodID midOpenFileDescriptor; +static jmethodID midShowFileDialog; /* audio manager */ static jclass mAudioManagerClass; @@ -640,6 +646,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl midShowTextInput = (*env)->GetStaticMethodID(env, mActivityClass, "showTextInput", "(IIII)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"); if (!midClipboardGetText || !midClipboardHasText || @@ -670,7 +677,8 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl !midShouldMinimizeOnFocusLoss || !midShowTextInput || !midSupportsRelativeMouse || - !midOpenFileDescriptor) { + !midOpenFileDescriptor || + !midShowFileDialog) { __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some Java callbacks, do you have the latest version of SDLActivity.java?"); } @@ -2823,4 +2831,138 @@ int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode) return fd; } +static struct AndroidFileDialog +{ + int request_code; + SDL_DialogFileCallback callback; + void *userdata; +} mAndroidFileDialogData; + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeFileDialog)( + JNIEnv *env, jclass jcls, + jint requestCode, jobjectArray fileList, jint filter) +{ + if (mAndroidFileDialogData.callback != NULL && mAndroidFileDialogData.request_code == requestCode) { + if (fileList == NULL) { + SDL_SetError("Unspecified error in JNI"); + mAndroidFileDialogData.callback(mAndroidFileDialogData.userdata, NULL, -1); + mAndroidFileDialogData.callback = NULL; + return; + } + + /* Convert fileList to string */ + size_t count = (*env)->GetArrayLength(env, fileList); + char **charFileList = SDL_calloc(sizeof(char*), count + 1); + + if (charFileList == NULL) { + SDL_OutOfMemory(); + mAndroidFileDialogData.callback(mAndroidFileDialogData.userdata, NULL, -1); + mAndroidFileDialogData.callback = NULL; + return; + } + + /* Convert to UTF-8 */ + /* TODO: Fix modified UTF-8 to classic UTF-8 */ + for (int i = 0; i < count; i++) { + jstring string = (*env)->GetObjectArrayElement(env, fileList, i); + if (!string) { + continue; + } + + const char *utf8string = (*env)->GetStringUTFChars(env, string, NULL); + if (!utf8string) { + (*env)->DeleteLocalRef(env, string); + continue; + } + + char *newFile = SDL_strdup(utf8string); + if (!newFile) { + (*env)->ReleaseStringUTFChars(env, string, utf8string); + (*env)->DeleteLocalRef(env, string); + SDL_OutOfMemory(); + mAndroidFileDialogData.callback(mAndroidFileDialogData.userdata, NULL, -1); + mAndroidFileDialogData.callback = NULL; + + /* Cleanup memory */ + for (int j = 0; j < i; j++) { + SDL_free(charFileList[j]); + } + SDL_free(charFileList); + return; + } + + charFileList[i] = newFile; + (*env)->ReleaseStringUTFChars(env, string, utf8string); + (*env)->DeleteLocalRef(env, string); + } + + /* Call user-provided callback */ + SDL_ClearError(); + mAndroidFileDialogData.callback(mAndroidFileDialogData.userdata, (const char *const *) charFileList, filter); + mAndroidFileDialogData.callback = NULL; + + /* Cleanup memory */ + for (int i = 0; i < count; i++) { + SDL_free(charFileList[i]); + } + SDL_free(charFileList); + } +} + +SDL_bool Android_JNI_OpenFileDialog( + SDL_DialogFileCallback callback, void* userdata, + const SDL_DialogFileFilter *filters, SDL_bool forwrite, SDL_bool multiple) +{ + if (mAndroidFileDialogData.callback != NULL) { + SDL_SetError("Only one file dialog can be run at a time."); + return SDL_FALSE; + } + + if (forwrite) { + multiple = SDL_FALSE; + } + + JNIEnv *env = Android_JNI_GetEnv(); + + /* Setup filters */ + jobjectArray filtersArray = NULL; + if (filters) { + /* Count how many filters */ + int count = 0; + for (const SDL_DialogFileFilter *f = filters; f->name != NULL && f->pattern != NULL; f++) { + count++; + } + + jclass stringClass = (*env)->FindClass(env, "java/lang/String"); + filtersArray = (*env)->NewObjectArray(env, count, stringClass, NULL); + + /* Convert to string */ + for (int i = 0; i < count; i++) { + jstring str = (*env)->NewStringUTF(env, filters[i].pattern); + (*env)->SetObjectArrayElement(env, filtersArray, i, str); + (*env)->DeleteLocalRef(env, str); + } + } + + /* Setup data */ + static SDL_AtomicInt next_request_code; + mAndroidFileDialogData.request_code = SDL_AtomicAdd(&next_request_code, 1); + mAndroidFileDialogData.userdata = userdata; + mAndroidFileDialogData.callback = callback; + + /* Invoke JNI */ + jboolean success = (*env)->CallStaticBooleanMethod(env, mActivityClass, + midShowFileDialog, filtersArray, (jboolean) multiple, (jboolean) forwrite, mAndroidFileDialogData.request_code); + (*env)->DeleteLocalRef(env, filtersArray); + if (!success) { + mAndroidFileDialogData.callback = NULL; + SDL_AtomicAdd(&next_request_code, -1); + SDL_SetError("Unspecified error in JNI"); + + return SDL_FALSE; + } + + return SDL_TRUE; +} + #endif /* SDL_PLATFORM_ANDROID */ diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h index 3ad8901430..db56846b73 100644 --- a/src/core/android/SDL_android.h +++ b/src/core/android/SDL_android.h @@ -142,6 +142,10 @@ void Android_ActivityMutex_Lock(void); void Android_ActivityMutex_Unlock(void); void Android_ActivityMutex_Lock_Running(void); +/* File Dialogs */ +SDL_bool Android_JNI_OpenFileDialog(SDL_DialogFileCallback callback, void* userdata, + const SDL_DialogFileFilter *filters, SDL_bool forwrite, SDL_bool multiple); + /* Ends C function definitions when using C++ */ #ifdef __cplusplus /* *INDENT-OFF* */ diff --git a/src/dialog/android/SDL_androiddialog.c b/src/dialog/android/SDL_androiddialog.c new file mode 100644 index 0000000000..c6418f162b --- /dev/null +++ b/src/dialog/android/SDL_androiddialog.c @@ -0,0 +1,45 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "SDL_internal.h" +#include "../../core/android/SDL_android.h" + +void SDLCALL SDL_ShowOpenFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, const char *default_location, SDL_bool allow_many) +{ + if (!Android_JNI_OpenFileDialog(callback, userdata, filters, SDL_FALSE, allow_many)) { + /* SDL_SetError is already called when it fails */ + callback(userdata, NULL, -1); + } +} + +void SDLCALL SDL_ShowSaveFileDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const SDL_DialogFileFilter *filters, const char *default_location) +{ + if (!Android_JNI_OpenFileDialog(callback, userdata, filters, SDL_TRUE, SDL_FALSE)) { + /* SDL_SetError is already called when it fails */ + callback(userdata, NULL, -1); + } +} + +void SDLCALL SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void *userdata, SDL_Window *window, const char *default_location, SDL_bool allow_many) +{ + SDL_Unsupported(); + callback(userdata, NULL, -1); +}