mirror of https://github.com/libsdl-org/SDL
Android: Implement open and save file dialog.
This commit is contained in:
parent
ea1904eda1
commit
86fada6faa
|
@ -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) \
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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* */
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
|
||||
|
||||
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);
|
||||
}
|
Loading…
Reference in New Issue