Android: Implement open and save file dialog.

This commit is contained in:
Miku AuahDark 2024-05-06 12:14:20 +08:00 committed by Sam Lantinga
parent ea1904eda1
commit 86fada6faa
7 changed files with 312 additions and 3 deletions

View File

@ -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) \

View File

@ -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)

View File

@ -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 {

View File

@ -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;
}
}
/**

View File

@ -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 */

View File

@ -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* */

View File

@ -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);
}