From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- contrib/SDL-3.2.8/src/core/android/SDL_android.c | 2824 ++++++++++++++++++++++ contrib/SDL-3.2.8/src/core/android/SDL_android.h | 163 ++ 2 files changed, 2987 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/core/android/SDL_android.c create mode 100644 contrib/SDL-3.2.8/src/core/android/SDL_android.h (limited to 'contrib/SDL-3.2.8/src/core/android') diff --git a/contrib/SDL-3.2.8/src/core/android/SDL_android.c b/contrib/SDL-3.2.8/src/core/android/SDL_android.c new file mode 100644 index 0000000..daf0f29 --- /dev/null +++ b/contrib/SDL-3.2.8/src/core/android/SDL_android.c @@ -0,0 +1,2824 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 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" + +#ifdef SDL_PLATFORM_ANDROID + +#include "SDL_android.h" + +#include "../../events/SDL_events_c.h" +#include "../../video/android/SDL_androidkeyboard.h" +#include "../../video/android/SDL_androidmouse.h" +#include "../../video/android/SDL_androidtouch.h" +#include "../../video/android/SDL_androidpen.h" +#include "../../video/android/SDL_androidvideo.h" +#include "../../video/android/SDL_androidwindow.h" +#include "../../joystick/android/SDL_sysjoystick_c.h" +#include "../../haptic/android/SDL_syshaptic_c.h" +#include "../../hidapi/android/hid.h" +#include "../../SDL_hints_c.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define SDL_JAVA_PREFIX org_libsdl_app +#define CONCAT1(prefix, class, function) CONCAT2(prefix, class, function) +#define CONCAT2(prefix, class, function) Java_##prefix##_##class##_##function +#define SDL_JAVA_INTERFACE(function) CONCAT1(SDL_JAVA_PREFIX, SDLActivity, function) +#define SDL_JAVA_AUDIO_INTERFACE(function) CONCAT1(SDL_JAVA_PREFIX, SDLAudioManager, function) +#define SDL_JAVA_CONTROLLER_INTERFACE(function) CONCAT1(SDL_JAVA_PREFIX, SDLControllerManager, function) +#define SDL_JAVA_INTERFACE_INPUT_CONNECTION(function) CONCAT1(SDL_JAVA_PREFIX, SDLInputConnection, function) + +// Audio encoding definitions +#define ENCODING_PCM_8BIT 3 +#define ENCODING_PCM_16BIT 2 +#define ENCODING_PCM_FLOAT 4 + +// Java class SDLActivity +JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetVersion)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeInitMainThread)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeCleanupMainThread)( + JNIEnv *env, jclass cls); + +JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeRunMain)( + JNIEnv *env, jclass cls, + jstring library, jstring function, jobject array); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDropFile)( + JNIEnv *env, jclass jcls, + jstring filename); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetScreenResolution)( + JNIEnv *env, jclass jcls, + jint surfaceWidth, jint surfaceHeight, + jint deviceWidth, jint deviceHeight, jfloat density, jfloat rate); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeResize)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceCreated)( + JNIEnv *env, jclass jcls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceChanged)( + JNIEnv *env, jclass jcls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceDestroyed)( + JNIEnv *env, jclass jcls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyDown)( + JNIEnv *env, jclass jcls, + jint keycode); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyUp)( + JNIEnv *env, jclass jcls, + jint keycode); + +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(onNativeSoftReturnKey)( + JNIEnv *env, jclass jcls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyboardFocusLost)( + JNIEnv *env, jclass jcls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeTouch)( + JNIEnv *env, jclass jcls, + jint touch_device_id_in, jint pointer_finger_id_in, + jint action, jfloat x, jfloat y, jfloat p); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeMouse)( + JNIEnv *env, jclass jcls, + jint button, jint action, jfloat x, jfloat y, jboolean relative); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePen)( + JNIEnv *env, jclass jcls, + jint pen_id_in, jint button, jint action, jfloat x, jfloat y, jfloat p); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeAccel)( + JNIEnv *env, jclass jcls, + jfloat x, jfloat y, jfloat z); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeClipboardChanged)( + JNIEnv *env, jclass jcls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeLowMemory)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeLocaleChanged)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDarkModeChanged)( + JNIEnv *env, jclass cls, jboolean enabled); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSendQuit)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeQuit)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePause)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeResume)( + JNIEnv *env, jclass cls); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeFocusChanged)( + JNIEnv *env, jclass cls, jboolean hasFocus); + +JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetHint)( + JNIEnv *env, jclass cls, + jstring name); + +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeGetHintBoolean)( + JNIEnv *env, jclass cls, + jstring name, jboolean default_value); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetenv)( + JNIEnv *env, jclass cls, + jstring name, jstring value); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetNaturalOrientation)( + JNIEnv *env, jclass cls, + jint orientation); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeRotationChanged)( + JNIEnv *env, jclass cls, + jint rotation); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeInsetsChanged)( + JNIEnv *env, jclass cls, + jint left, jint right, jint top, jint bottom); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeAddTouch)( + JNIEnv *env, jclass cls, + jint touchId, jstring name); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePermissionResult)( + JNIEnv *env, jclass cls, + jint requestCode, jboolean result); + +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeAllowRecreateActivity)( + JNIEnv *env, jclass jcls); + +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) }, + { "nativeInitMainThread", "()V", SDL_JAVA_INTERFACE(nativeInitMainThread) }, + { "nativeCleanupMainThread", "()V", SDL_JAVA_INTERFACE(nativeCleanupMainThread) }, + { "nativeRunMain", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)I", SDL_JAVA_INTERFACE(nativeRunMain) }, + { "onNativeDropFile", "(Ljava/lang/String;)V", SDL_JAVA_INTERFACE(onNativeDropFile) }, + { "nativeSetScreenResolution", "(IIIIFF)V", SDL_JAVA_INTERFACE(nativeSetScreenResolution) }, + { "onNativeResize", "()V", SDL_JAVA_INTERFACE(onNativeResize) }, + { "onNativeSurfaceCreated", "()V", SDL_JAVA_INTERFACE(onNativeSurfaceCreated) }, + { "onNativeSurfaceChanged", "()V", SDL_JAVA_INTERFACE(onNativeSurfaceChanged) }, + { "onNativeSurfaceDestroyed", "()V", SDL_JAVA_INTERFACE(onNativeSurfaceDestroyed) }, + { "onNativeKeyDown", "(I)V", SDL_JAVA_INTERFACE(onNativeKeyDown) }, + { "onNativeKeyUp", "(I)V", SDL_JAVA_INTERFACE(onNativeKeyUp) }, + { "onNativeSoftReturnKey", "()Z", SDL_JAVA_INTERFACE(onNativeSoftReturnKey) }, + { "onNativeKeyboardFocusLost", "()V", SDL_JAVA_INTERFACE(onNativeKeyboardFocusLost) }, + { "onNativeTouch", "(IIIFFF)V", SDL_JAVA_INTERFACE(onNativeTouch) }, + { "onNativeMouse", "(IIFFZ)V", SDL_JAVA_INTERFACE(onNativeMouse) }, + { "onNativePen", "(IIIFFF)V", SDL_JAVA_INTERFACE(onNativePen) }, + { "onNativeAccel", "(FFF)V", SDL_JAVA_INTERFACE(onNativeAccel) }, + { "onNativeClipboardChanged", "()V", SDL_JAVA_INTERFACE(onNativeClipboardChanged) }, + { "nativeLowMemory", "()V", SDL_JAVA_INTERFACE(nativeLowMemory) }, + { "onNativeLocaleChanged", "()V", SDL_JAVA_INTERFACE(onNativeLocaleChanged) }, + { "onNativeDarkModeChanged", "(Z)V", SDL_JAVA_INTERFACE(onNativeDarkModeChanged) }, + { "nativeSendQuit", "()V", SDL_JAVA_INTERFACE(nativeSendQuit) }, + { "nativeQuit", "()V", SDL_JAVA_INTERFACE(nativeQuit) }, + { "nativePause", "()V", SDL_JAVA_INTERFACE(nativePause) }, + { "nativeResume", "()V", SDL_JAVA_INTERFACE(nativeResume) }, + { "nativeFocusChanged", "(Z)V", SDL_JAVA_INTERFACE(nativeFocusChanged) }, + { "nativeGetHint", "(Ljava/lang/String;)Ljava/lang/String;", SDL_JAVA_INTERFACE(nativeGetHint) }, + { "nativeGetHintBoolean", "(Ljava/lang/String;Z)Z", SDL_JAVA_INTERFACE(nativeGetHintBoolean) }, + { "nativeSetenv", "(Ljava/lang/String;Ljava/lang/String;)V", SDL_JAVA_INTERFACE(nativeSetenv) }, + { "nativeSetNaturalOrientation", "(I)V", SDL_JAVA_INTERFACE(nativeSetNaturalOrientation) }, + { "onNativeRotationChanged", "(I)V", SDL_JAVA_INTERFACE(onNativeRotationChanged) }, + { "onNativeInsetsChanged", "(IIII)V", SDL_JAVA_INTERFACE(onNativeInsetsChanged) }, + { "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) }, + { "onNativeFileDialog", "(I[Ljava/lang/String;I)V", SDL_JAVA_INTERFACE(onNativeFileDialog) } +}; + +// Java class SDLInputConnection +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE_INPUT_CONNECTION(nativeCommitText)( + JNIEnv *env, jclass cls, + jstring text, jint newCursorPosition); + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE_INPUT_CONNECTION(nativeGenerateScancodeForUnichar)( + JNIEnv *env, jclass cls, + jchar chUnicode); + +static JNINativeMethod SDLInputConnection_tab[] = { + { "nativeCommitText", "(Ljava/lang/String;I)V", SDL_JAVA_INTERFACE_INPUT_CONNECTION(nativeCommitText) }, + { "nativeGenerateScancodeForUnichar", "(C)V", SDL_JAVA_INTERFACE_INPUT_CONNECTION(nativeGenerateScancodeForUnichar) } +}; + +// Java class SDLAudioManager +JNIEXPORT void JNICALL SDL_JAVA_AUDIO_INTERFACE(nativeSetupJNI)( + JNIEnv *env, jclass jcls); + +JNIEXPORT void JNICALL + SDL_JAVA_AUDIO_INTERFACE(addAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, jstring name, + jint device_id); + +JNIEXPORT void JNICALL + SDL_JAVA_AUDIO_INTERFACE(removeAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, + jint device_id); + +static JNINativeMethod SDLAudioManager_tab[] = { + { "nativeSetupJNI", "()I", SDL_JAVA_AUDIO_INTERFACE(nativeSetupJNI) }, + { "addAudioDevice", "(ZLjava/lang/String;I)V", SDL_JAVA_AUDIO_INTERFACE(addAudioDevice) }, + { "removeAudioDevice", "(ZI)V", SDL_JAVA_AUDIO_INTERFACE(removeAudioDevice) } +}; + +// Java class SDLControllerManager +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI)( + JNIEnv *env, jclass jcls); + +JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( + JNIEnv *env, jclass jcls, + jint device_id, jint keycode); + +JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp)( + JNIEnv *env, jclass jcls, + jint device_id, jint keycode); + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy)( + JNIEnv *env, jclass jcls, + jint device_id, jint axis, jfloat value); + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat)( + JNIEnv *env, jclass jcls, + jint device_id, jint hat_id, jint x, jint y); + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick)( + JNIEnv *env, jclass jcls, + jint device_id, jstring device_name, jstring device_desc, jint vendor_id, jint product_id, + jint button_mask, jint naxes, jint axis_mask, jint nhats, jboolean can_rumble); + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick)( + JNIEnv *env, jclass jcls, + jint device_id); + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddHaptic)( + JNIEnv *env, jclass jcls, + jint device_id, jstring device_name); + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic)( + JNIEnv *env, jclass jcls, + jint device_id); + +static JNINativeMethod SDLControllerManager_tab[] = { + { "nativeSetupJNI", "()I", SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI) }, + { "onNativePadDown", "(II)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown) }, + { "onNativePadUp", "(II)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp) }, + { "onNativeJoy", "(IIF)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy) }, + { "onNativeHat", "(IIII)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat) }, + { "nativeAddJoystick", "(ILjava/lang/String;Ljava/lang/String;IIIIIIZ)V", SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick) }, + { "nativeRemoveJoystick", "(I)V", SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick) }, + { "nativeAddHaptic", "(ILjava/lang/String;)V", SDL_JAVA_CONTROLLER_INTERFACE(nativeAddHaptic) }, + { "nativeRemoveHaptic", "(I)V", SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic) } +}; + +// Uncomment this to log messages entering and exiting methods in this file +// #define DEBUG_JNI + +static void checkJNIReady(void); + +/******************************************************************************* + This file links the Java side of Android with libsdl +*******************************************************************************/ +#include + +/******************************************************************************* + Globals +*******************************************************************************/ +static pthread_key_t mThreadKey; +static pthread_once_t key_once = PTHREAD_ONCE_INIT; +static JavaVM *mJavaVM = NULL; + +// Main activity +static jclass mActivityClass; + +// method signatures +static jmethodID midClipboardGetText; +static jmethodID midClipboardHasText; +static jmethodID midClipboardSetText; +static jmethodID midCreateCustomCursor; +static jmethodID midDestroyCustomCursor; +static jmethodID midGetContext; +static jmethodID midGetManifestEnvironmentVariables; +static jmethodID midGetNativeSurface; +static jmethodID midInitTouch; +static jmethodID midIsAndroidTV; +static jmethodID midIsChromebook; +static jmethodID midIsDeXMode; +static jmethodID midIsScreenKeyboardShown; +static jmethodID midIsTablet; +static jmethodID midManualBackButton; +static jmethodID midMinimizeWindow; +static jmethodID midOpenURL; +static jmethodID midRequestPermission; +static jmethodID midShowToast; +static jmethodID midSendMessage; +static jmethodID midSetActivityTitle; +static jmethodID midSetCustomCursor; +static jmethodID midSetOrientation; +static jmethodID midSetRelativeMouseEnabled; +static jmethodID midSetSystemCursor; +static jmethodID midSetWindowStyle; +static jmethodID midShouldMinimizeOnFocusLoss; +static jmethodID midShowTextInput; +static jmethodID midSupportsRelativeMouse; +static jmethodID midOpenFileDescriptor; +static jmethodID midShowFileDialog; + +// audio manager +static jclass mAudioManagerClass; + +// method signatures +static jmethodID midRegisterAudioDeviceCallback; +static jmethodID midUnregisterAudioDeviceCallback; +static jmethodID midAudioSetThreadPriority; + +// controller manager +static jclass mControllerManagerClass; + +// method signatures +static jmethodID midPollInputDevices; +static jmethodID midPollHapticDevices; +static jmethodID midHapticRun; +static jmethodID midHapticRumble; +static jmethodID midHapticStop; + +// Accelerometer data storage +static SDL_DisplayOrientation displayNaturalOrientation; +static SDL_DisplayOrientation displayCurrentOrientation; +static float fLastAccelerometer[3]; +static bool bHasNewData; + +static bool bHasEnvironmentVariables; + +// Android AssetManager +static void Internal_Android_Create_AssetManager(void); +static void Internal_Android_Destroy_AssetManager(void); +static AAssetManager *asset_manager = NULL; +static jobject javaAssetManagerRef = 0; + +static SDL_Mutex *Android_ActivityMutex = NULL; +static SDL_Mutex *Android_LifecycleMutex = NULL; +static SDL_Semaphore *Android_LifecycleEventSem = NULL; +static SDL_AndroidLifecycleEvent Android_LifecycleEvents[SDL_NUM_ANDROID_LIFECYCLE_EVENTS]; +static int Android_NumLifecycleEvents; + +/******************************************************************************* + Functions called by JNI +*******************************************************************************/ + +/* From http://developer.android.com/guide/practices/jni.html + * All threads are Linux threads, scheduled by the kernel. + * They're usually started from managed code (using Thread.start), but they can also be created elsewhere and then + * attached to the JavaVM. For example, a thread started with pthread_create can be attached with the + * JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, + * and cannot make JNI calls. + * Attaching a natively-created thread causes a java.lang.Thread object to be constructed and added to the "main" + * ThreadGroup, making it visible to the debugger. Calling AttachCurrentThread on an already-attached thread + * is a no-op. + * Note: You can call this function any number of times for the same thread, there's no harm in it + */ + +/* From http://developer.android.com/guide/practices/jni.html + * Threads attached through JNI must call DetachCurrentThread before they exit. If coding this directly is awkward, + * in Android 2.0 (Eclair) and higher you can use pthread_key_create to define a destructor function that will be + * called before the thread exits, and call DetachCurrentThread from there. (Use that key with pthread_setspecific + * to store the JNIEnv in thread-local-storage; that way it'll be passed into your destructor as the argument.) + * Note: The destructor is not called unless the stored value is != NULL + * Note: You can call this function any number of times for the same thread, there's no harm in it + * (except for some lost CPU cycles) + */ + +// Set local storage value +static bool Android_JNI_SetEnv(JNIEnv *env) +{ + int status = pthread_setspecific(mThreadKey, env); + if (status < 0) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed pthread_setspecific() in Android_JNI_SetEnv() (err=%d)", status); + return false; + } + return true; +} + +// Get local storage value +JNIEnv *Android_JNI_GetEnv(void) +{ + // Get JNIEnv from the Thread local storage + JNIEnv *env = pthread_getspecific(mThreadKey); + if (!env) { + // If it fails, try to attach ! (e.g the thread isn't created with SDL_CreateThread() + int status; + + // There should be a JVM + if (!mJavaVM) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed, there is no JavaVM"); + return NULL; + } + + /* Attach the current thread to the JVM and get a JNIEnv. + * It will be detached by pthread_create destructor 'Android_JNI_ThreadDestroyed' */ + status = (*mJavaVM)->AttachCurrentThread(mJavaVM, &env, NULL); + if (status < 0) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed to attach current thread (err=%d)", status); + return NULL; + } + + // Save JNIEnv into the Thread local storage + if (!Android_JNI_SetEnv(env)) { + return NULL; + } + } + + return env; +} + +// Set up an external thread for using JNI with Android_JNI_GetEnv() +bool Android_JNI_SetupThread(void) +{ + JNIEnv *env; + int status; + + // There should be a JVM + if (!mJavaVM) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed, there is no JavaVM"); + return false; + } + + /* Attach the current thread to the JVM and get a JNIEnv. + * It will be detached by pthread_create destructor 'Android_JNI_ThreadDestroyed' */ + status = (*mJavaVM)->AttachCurrentThread(mJavaVM, &env, NULL); + if (status < 0) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed to attach current thread (err=%d)", status); + return false; + } + + // Save JNIEnv into the Thread local storage + if (!Android_JNI_SetEnv(env)) { + return false; + } + + return true; +} + +// Destructor called for each thread where mThreadKey is not NULL +static void Android_JNI_ThreadDestroyed(void *value) +{ + // The thread is being destroyed, detach it from the Java VM and set the mThreadKey value to NULL as required + JNIEnv *env = (JNIEnv *)value; + if (env) { + (*mJavaVM)->DetachCurrentThread(mJavaVM); + Android_JNI_SetEnv(NULL); + } +} + +// Creation of local storage mThreadKey +static void Android_JNI_CreateKey(void) +{ + int status = pthread_key_create(&mThreadKey, Android_JNI_ThreadDestroyed); + if (status < 0) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Error initializing mThreadKey with pthread_key_create() (err=%d)", status); + } +} + +static void Android_JNI_CreateKey_once(void) +{ + int status = pthread_once(&key_once, Android_JNI_CreateKey); + if (status < 0) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Error initializing mThreadKey with pthread_once() (err=%d)", status); + } +} + +static void register_methods(JNIEnv *env, const char *classname, JNINativeMethod *methods, int nb) +{ + jclass clazz = (*env)->FindClass(env, classname); + if (!clazz || (*env)->RegisterNatives(env, clazz, methods, nb) < 0) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed to register methods of %s", classname); + return; + } +} + +// Library init +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) +{ + JNIEnv *env = NULL; + + mJavaVM = vm; + + if ((*mJavaVM)->GetEnv(mJavaVM, (void **)&env, JNI_VERSION_1_4) != JNI_OK) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed to get JNI Env"); + return JNI_VERSION_1_4; + } + + register_methods(env, "org/libsdl/app/SDLActivity", SDLActivity_tab, SDL_arraysize(SDLActivity_tab)); + register_methods(env, "org/libsdl/app/SDLInputConnection", SDLInputConnection_tab, SDL_arraysize(SDLInputConnection_tab)); + register_methods(env, "org/libsdl/app/SDLAudioManager", SDLAudioManager_tab, SDL_arraysize(SDLAudioManager_tab)); + register_methods(env, "org/libsdl/app/SDLControllerManager", SDLControllerManager_tab, SDL_arraysize(SDLControllerManager_tab)); + register_methods(env, "org/libsdl/app/HIDDeviceManager", HIDDeviceManager_tab, SDL_arraysize(HIDDeviceManager_tab)); + + return JNI_VERSION_1_4; +} + +void checkJNIReady(void) +{ + if (!mActivityClass || !mAudioManagerClass || !mControllerManagerClass) { + // We aren't fully initialized, let's just return. + return; + } + + SDL_SetMainReady(); +} + +// Get SDL version -- called before SDL_main() to verify JNI bindings +JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetVersion)(JNIEnv *env, jclass cls) +{ + char version[128]; + + SDL_snprintf(version, sizeof(version), "%d.%d.%d", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_MICRO_VERSION); + + return (*env)->NewStringUTF(env, version); +} + +// Activity initialization -- called before SDL_main() to initialize JNI bindings +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cls) +{ + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeSetupJNI()"); + + // Start with a clean slate + SDL_ClearError(); + + /* + * Create mThreadKey so we can keep track of the JNIEnv assigned to each thread + * Refer to http://developer.android.com/guide/practices/design/jni.html for the rationale behind this + */ + Android_JNI_CreateKey_once(); + + // Save JNIEnv of SDLActivity + Android_JNI_SetEnv(env); + + if (!mJavaVM) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to found a JavaVM"); + } + + /* Use a mutex to prevent concurrency issues between Java Activity and Native thread code, when using 'Android_Window'. + * (Eg. Java sending Touch events, while native code is destroying the main SDL_Window. ) + */ + if (!Android_ActivityMutex) { + Android_ActivityMutex = SDL_CreateMutex(); // Could this be created twice if onCreate() is called a second time ? + } + + if (!Android_ActivityMutex) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_ActivityMutex mutex"); + } + + Android_LifecycleMutex = SDL_CreateMutex(); + if (!Android_LifecycleMutex) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_LifecycleMutex mutex"); + } + + Android_LifecycleEventSem = SDL_CreateSemaphore(0); + if (!Android_LifecycleEventSem) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_LifecycleEventSem semaphore"); + } + + mActivityClass = (jclass)((*env)->NewGlobalRef(env, cls)); + + midClipboardGetText = (*env)->GetStaticMethodID(env, mActivityClass, "clipboardGetText", "()Ljava/lang/String;"); + midClipboardHasText = (*env)->GetStaticMethodID(env, mActivityClass, "clipboardHasText", "()Z"); + midClipboardSetText = (*env)->GetStaticMethodID(env, mActivityClass, "clipboardSetText", "(Ljava/lang/String;)V"); + midCreateCustomCursor = (*env)->GetStaticMethodID(env, mActivityClass, "createCustomCursor", "([IIIII)I"); + midDestroyCustomCursor = (*env)->GetStaticMethodID(env, mActivityClass, "destroyCustomCursor", "(I)V"); + midGetContext = (*env)->GetStaticMethodID(env, mActivityClass, "getContext", "()Landroid/content/Context;"); + midGetManifestEnvironmentVariables = (*env)->GetStaticMethodID(env, mActivityClass, "getManifestEnvironmentVariables", "()Z"); + midGetNativeSurface = (*env)->GetStaticMethodID(env, mActivityClass, "getNativeSurface", "()Landroid/view/Surface;"); + midInitTouch = (*env)->GetStaticMethodID(env, mActivityClass, "initTouch", "()V"); + midIsAndroidTV = (*env)->GetStaticMethodID(env, mActivityClass, "isAndroidTV", "()Z"); + midIsChromebook = (*env)->GetStaticMethodID(env, mActivityClass, "isChromebook", "()Z"); + midIsDeXMode = (*env)->GetStaticMethodID(env, mActivityClass, "isDeXMode", "()Z"); + midIsScreenKeyboardShown = (*env)->GetStaticMethodID(env, mActivityClass, "isScreenKeyboardShown", "()Z"); + midIsTablet = (*env)->GetStaticMethodID(env, mActivityClass, "isTablet", "()Z"); + midManualBackButton = (*env)->GetStaticMethodID(env, mActivityClass, "manualBackButton", "()V"); + midMinimizeWindow = (*env)->GetStaticMethodID(env, mActivityClass, "minimizeWindow", "()V"); + midOpenURL = (*env)->GetStaticMethodID(env, mActivityClass, "openURL", "(Ljava/lang/String;)Z"); + midRequestPermission = (*env)->GetStaticMethodID(env, mActivityClass, "requestPermission", "(Ljava/lang/String;I)V"); + midShowToast = (*env)->GetStaticMethodID(env, mActivityClass, "showToast", "(Ljava/lang/String;IIII)Z"); + midSendMessage = (*env)->GetStaticMethodID(env, mActivityClass, "sendMessage", "(II)Z"); + midSetActivityTitle = (*env)->GetStaticMethodID(env, mActivityClass, "setActivityTitle", "(Ljava/lang/String;)Z"); + midSetCustomCursor = (*env)->GetStaticMethodID(env, mActivityClass, "setCustomCursor", "(I)Z"); + midSetOrientation = (*env)->GetStaticMethodID(env, mActivityClass, "setOrientation", "(IIZLjava/lang/String;)V"); + midSetRelativeMouseEnabled = (*env)->GetStaticMethodID(env, mActivityClass, "setRelativeMouseEnabled", "(Z)Z"); + midSetSystemCursor = (*env)->GetStaticMethodID(env, mActivityClass, "setSystemCursor", "(I)Z"); + midSetWindowStyle = (*env)->GetStaticMethodID(env, mActivityClass, "setWindowStyle", "(Z)V"); + midShouldMinimizeOnFocusLoss = (*env)->GetStaticMethodID(env, mActivityClass, "shouldMinimizeOnFocusLoss", "()Z"); + 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"); + + if (!midClipboardGetText || + !midClipboardHasText || + !midClipboardSetText || + !midCreateCustomCursor || + !midDestroyCustomCursor || + !midGetContext || + !midGetManifestEnvironmentVariables || + !midGetNativeSurface || + !midInitTouch || + !midIsAndroidTV || + !midIsChromebook || + !midIsDeXMode || + !midIsScreenKeyboardShown || + !midIsTablet || + !midManualBackButton || + !midMinimizeWindow || + !midOpenURL || + !midRequestPermission || + !midShowToast || + !midSendMessage || + !midSetActivityTitle || + !midSetCustomCursor || + !midSetOrientation || + !midSetRelativeMouseEnabled || + !midSetSystemCursor || + !midSetWindowStyle || + !midShouldMinimizeOnFocusLoss || + !midShowTextInput || + !midSupportsRelativeMouse || + !midOpenFileDescriptor || + !midShowFileDialog) { + __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some Java callbacks, do you have the latest version of SDLActivity.java?"); + } + + checkJNIReady(); +} + +// Audio initialization -- called before SDL_main() to initialize JNI bindings +JNIEXPORT void JNICALL SDL_JAVA_AUDIO_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cls) +{ + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "AUDIO nativeSetupJNI()"); + + mAudioManagerClass = (jclass)((*env)->NewGlobalRef(env, cls)); + + midRegisterAudioDeviceCallback = (*env)->GetStaticMethodID(env, mAudioManagerClass, + "registerAudioDeviceCallback", + "()V"); + midUnregisterAudioDeviceCallback = (*env)->GetStaticMethodID(env, mAudioManagerClass, + "unregisterAudioDeviceCallback", + "()V"); + midAudioSetThreadPriority = (*env)->GetStaticMethodID(env, mAudioManagerClass, + "audioSetThreadPriority", "(ZI)V"); + + if (!midRegisterAudioDeviceCallback || !midUnregisterAudioDeviceCallback || !midAudioSetThreadPriority) { + __android_log_print(ANDROID_LOG_WARN, "SDL", + "Missing some Java callbacks, do you have the latest version of SDLAudioManager.java?"); + } + + checkJNIReady(); +} + +// Controller initialization -- called before SDL_main() to initialize JNI bindings +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cls) +{ + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "CONTROLLER nativeSetupJNI()"); + + mControllerManagerClass = (jclass)((*env)->NewGlobalRef(env, cls)); + + midPollInputDevices = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "pollInputDevices", "()V"); + midPollHapticDevices = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "pollHapticDevices", "()V"); + midHapticRun = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "hapticRun", "(IFI)V"); + midHapticRumble = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "hapticRumble", "(IFFI)V"); + midHapticStop = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "hapticStop", "(I)V"); + + if (!midPollInputDevices || !midPollHapticDevices || !midHapticRun || !midHapticRumble || !midHapticStop) { + __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some Java callbacks, do you have the latest version of SDLControllerManager.java?"); + } + + checkJNIReady(); +} + +// SDL main function prototype +typedef int (*SDL_main_func)(int argc, char *argv[]); + +static int run_count = 0; + +JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeCheckSDLThreadCounter)( + JNIEnv *env, jclass jcls) +{ + int tmp = run_count; + run_count += 1; + return tmp; +} + +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeAllowRecreateActivity)( + JNIEnv *env, jclass jcls) +{ + return SDL_GetHintBoolean(SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY, false); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeInitMainThread)( + JNIEnv *env, jclass jcls) +{ + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeInitSDLThread() %d time", run_count); + run_count += 1; + + // Save JNIEnv of SDLThread + Android_JNI_SetEnv(env); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeCleanupMainThread)( + JNIEnv *env, jclass jcls) +{ + /* This is a Java thread, it doesn't need to be Detached from the JVM. + * Set to mThreadKey value to NULL not to call pthread_create destructor 'Android_JNI_ThreadDestroyed' */ + Android_JNI_SetEnv(NULL); +} + +// Start up the SDL app +JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeRunMain)(JNIEnv *env, jclass cls, jstring library, jstring function, jobject array) +{ + int status = -1; + const char *library_file; + void *library_handle; + + library_file = (*env)->GetStringUTFChars(env, library, NULL); + library_handle = dlopen(library_file, RTLD_GLOBAL); + + if (library_handle == NULL) { + /* When deploying android app bundle format uncompressed native libs may not extract from apk to filesystem. + In this case we should use lib name without path. https://bugzilla.libsdl.org/show_bug.cgi?id=4739 */ + const char *library_name = SDL_strrchr(library_file, '/'); + if (library_name && *library_name) { + library_name += 1; + library_handle = dlopen(library_name, RTLD_GLOBAL); + } + } + + if (library_handle) { + const char *function_name; + SDL_main_func SDL_main; + + function_name = (*env)->GetStringUTFChars(env, function, NULL); + SDL_main = (SDL_main_func)dlsym(library_handle, function_name); + if (SDL_main) { + int i; + int argc; + int len; + char **argv; + bool isstack; + + // Prepare the arguments. + len = (*env)->GetArrayLength(env, array); + argv = SDL_small_alloc(char *, 1 + len + 1, &isstack); // !!! FIXME: check for NULL + argc = 0; + /* Use the name "app_process" so PHYSFS_platformCalcBaseDir() works. + https://github.com/love2d/love-android/issues/24 + */ + argv[argc++] = SDL_strdup("app_process"); + for (i = 0; i < len; ++i) { + char *arg = NULL; + jstring string = (*env)->GetObjectArrayElement(env, array, i); + if (string) { + const char *utf = (*env)->GetStringUTFChars(env, string, 0); + if (utf) { + arg = SDL_strdup(utf); + (*env)->ReleaseStringUTFChars(env, string, utf); + } + (*env)->DeleteLocalRef(env, string); + } + if (arg == NULL) { + arg = SDL_strdup(""); + } + argv[argc++] = arg; + } + argv[argc] = NULL; + + // Run the application. + status = SDL_main(argc, argv); + + // Release the arguments. + for (i = 0; i < argc; ++i) { + SDL_free(argv[i]); + } + SDL_small_free(argv, isstack); + + } else { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "nativeRunMain(): Couldn't find function %s in library %s", function_name, library_file); + } + (*env)->ReleaseStringUTFChars(env, function, function_name); + + dlclose(library_handle); + + } else { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "nativeRunMain(): Couldn't load library %s", library_file); + } + (*env)->ReleaseStringUTFChars(env, library, library_file); + + // Do not issue an exit or the whole application will terminate instead of just the SDL thread + // exit(status); + + return status; +} + +static int FindLifecycleEvent(SDL_AndroidLifecycleEvent event) +{ + for (int index = 0; index < Android_NumLifecycleEvents; ++index) { + if (Android_LifecycleEvents[index] == event) { + return index; + } + } + return -1; +} + +static void RemoveLifecycleEvent(int index) +{ + if (index < Android_NumLifecycleEvents - 1) { + SDL_memmove(&Android_LifecycleEvents[index], &Android_LifecycleEvents[index+1], (Android_NumLifecycleEvents - index - 1) * sizeof(Android_LifecycleEvents[index])); + } + --Android_NumLifecycleEvents; +} + +void Android_SendLifecycleEvent(SDL_AndroidLifecycleEvent event) +{ + SDL_LockMutex(Android_LifecycleMutex); + { + int index; + bool add_event = true; + + switch (event) { + case SDL_ANDROID_LIFECYCLE_WAKE: + // We don't need more than one wake queued + index = FindLifecycleEvent(SDL_ANDROID_LIFECYCLE_WAKE); + if (index >= 0) { + add_event = false; + } + break; + case SDL_ANDROID_LIFECYCLE_PAUSE: + // If we have a resume queued, just stay in the paused state + index = FindLifecycleEvent(SDL_ANDROID_LIFECYCLE_RESUME); + if (index >= 0) { + RemoveLifecycleEvent(index); + add_event = false; + } + break; + case SDL_ANDROID_LIFECYCLE_RESUME: + // If we have a pause queued, just stay in the resumed state + index = FindLifecycleEvent(SDL_ANDROID_LIFECYCLE_PAUSE); + if (index >= 0) { + RemoveLifecycleEvent(index); + add_event = false; + } + break; + case SDL_ANDROID_LIFECYCLE_LOWMEMORY: + // We don't need more than one low memory event queued + index = FindLifecycleEvent(SDL_ANDROID_LIFECYCLE_LOWMEMORY); + if (index >= 0) { + add_event = false; + } + break; + case SDL_ANDROID_LIFECYCLE_DESTROY: + // Remove all other events, we're done! + while (Android_NumLifecycleEvents > 0) { + RemoveLifecycleEvent(0); + } + break; + default: + SDL_assert(!"Sending unexpected lifecycle event"); + add_event = false; + break; + } + + if (add_event) { + SDL_assert(Android_NumLifecycleEvents < SDL_arraysize(Android_LifecycleEvents)); + Android_LifecycleEvents[Android_NumLifecycleEvents++] = event; + SDL_SignalSemaphore(Android_LifecycleEventSem); + } + } + SDL_UnlockMutex(Android_LifecycleMutex); +} + +bool Android_WaitLifecycleEvent(SDL_AndroidLifecycleEvent *event, Sint64 timeoutNS) +{ + bool got_event = false; + + while (!got_event && SDL_WaitSemaphoreTimeoutNS(Android_LifecycleEventSem, timeoutNS)) { + SDL_LockMutex(Android_LifecycleMutex); + { + if (Android_NumLifecycleEvents > 0) { + *event = Android_LifecycleEvents[0]; + RemoveLifecycleEvent(0); + got_event = true; + } + } + SDL_UnlockMutex(Android_LifecycleMutex); + } + return got_event; +} + +void Android_LockActivityMutex(void) +{ + SDL_LockMutex(Android_ActivityMutex); +} + +void Android_UnlockActivityMutex(void) +{ + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Drop file +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDropFile)( + JNIEnv *env, jclass jcls, + jstring filename) +{ + const char *path = (*env)->GetStringUTFChars(env, filename, NULL); + SDL_SendDropFile(NULL, NULL, path); + (*env)->ReleaseStringUTFChars(env, filename, path); + SDL_SendDropComplete(NULL); +} + +// Set screen resolution +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetScreenResolution)( + JNIEnv *env, jclass jcls, + jint surfaceWidth, jint surfaceHeight, + jint deviceWidth, jint deviceHeight, jfloat density, jfloat rate) +{ + SDL_LockMutex(Android_ActivityMutex); + + Android_SetScreenResolution(surfaceWidth, surfaceHeight, deviceWidth, deviceHeight, density, rate); + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Resize +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeResize)( + JNIEnv *env, jclass jcls) +{ + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + Android_SendResize(Android_Window); + } + + SDL_UnlockMutex(Android_ActivityMutex); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetNaturalOrientation)( + JNIEnv *env, jclass jcls, + jint orientation) +{ + displayNaturalOrientation = (SDL_DisplayOrientation)orientation; +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeRotationChanged)( + JNIEnv *env, jclass jcls, + jint rotation) +{ + SDL_LockMutex(Android_ActivityMutex); + + if (displayNaturalOrientation == SDL_ORIENTATION_LANDSCAPE) { + rotation += 90; + } + + switch (rotation % 360) { + case 0: + displayCurrentOrientation = SDL_ORIENTATION_PORTRAIT; + break; + case 90: + displayCurrentOrientation = SDL_ORIENTATION_LANDSCAPE; + break; + case 180: + displayCurrentOrientation = SDL_ORIENTATION_PORTRAIT_FLIPPED; + break; + case 270: + displayCurrentOrientation = SDL_ORIENTATION_LANDSCAPE_FLIPPED; + break; + default: + displayCurrentOrientation = SDL_ORIENTATION_UNKNOWN; + break; + } + + Android_SetOrientation(displayCurrentOrientation); + + SDL_UnlockMutex(Android_ActivityMutex); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeInsetsChanged)( + JNIEnv *env, jclass jcls, + jint left, jint right, jint top, jint bottom) +{ + SDL_LockMutex(Android_ActivityMutex); + + Android_SetWindowSafeAreaInsets(left, right, top, bottom); + + SDL_UnlockMutex(Android_ActivityMutex); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeAddTouch)( + JNIEnv *env, jclass cls, + jint touchId, jstring name) +{ + const char *utfname = (*env)->GetStringUTFChars(env, name, NULL); + + SDL_AddTouch((SDL_TouchID)touchId, SDL_TOUCH_DEVICE_DIRECT, utfname); + + (*env)->ReleaseStringUTFChars(env, name, utfname); +} + +JNIEXPORT void JNICALL +SDL_JAVA_AUDIO_INTERFACE(addAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, + jstring name, jint device_id) +{ +#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES + if (SDL_GetCurrentAudioDriver() != NULL) { + void *handle = (void *)((size_t)device_id); + if (!SDL_FindPhysicalAudioDeviceByHandle(handle)) { + const char *utf8name = (*env)->GetStringUTFChars(env, name, NULL); + SDL_AddAudioDevice(recording, SDL_strdup(utf8name), NULL, handle); + (*env)->ReleaseStringUTFChars(env, name, utf8name); + } + } +#endif +} + +JNIEXPORT void JNICALL +SDL_JAVA_AUDIO_INTERFACE(removeAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, + jint device_id) +{ +#if ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES + if (SDL_GetCurrentAudioDriver() != NULL) { + SDL_Log("Removing device with handle %d, recording %d", device_id, recording); + SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle((void *)((size_t)device_id))); + } +#endif +} + +// Paddown +JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( + JNIEnv *env, jclass jcls, + jint device_id, jint keycode) +{ +#ifdef SDL_JOYSTICK_ANDROID + return Android_OnPadDown(device_id, keycode); +#else + return false; +#endif // SDL_JOYSTICK_ANDROID +} + +// Padup +JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp)( + JNIEnv *env, jclass jcls, + jint device_id, jint keycode) +{ +#ifdef SDL_JOYSTICK_ANDROID + return Android_OnPadUp(device_id, keycode); +#else + return false; +#endif // SDL_JOYSTICK_ANDROID +} + +// Joy +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy)( + JNIEnv *env, jclass jcls, + jint device_id, jint axis, jfloat value) +{ +#ifdef SDL_JOYSTICK_ANDROID + Android_OnJoy(device_id, axis, value); +#endif // SDL_JOYSTICK_ANDROID +} + +// POV Hat +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat)( + JNIEnv *env, jclass jcls, + jint device_id, jint hat_id, jint x, jint y) +{ +#ifdef SDL_JOYSTICK_ANDROID + Android_OnHat(device_id, hat_id, x, y); +#endif // SDL_JOYSTICK_ANDROID +} + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick)( + JNIEnv *env, jclass jcls, + jint device_id, jstring device_name, jstring device_desc, + jint vendor_id, jint product_id, + jint button_mask, jint naxes, jint axis_mask, jint nhats, jboolean can_rumble) +{ +#ifdef SDL_JOYSTICK_ANDROID + const char *name = (*env)->GetStringUTFChars(env, device_name, NULL); + const char *desc = (*env)->GetStringUTFChars(env, device_desc, NULL); + + Android_AddJoystick(device_id, name, desc, vendor_id, product_id, button_mask, naxes, axis_mask, nhats, can_rumble); + + (*env)->ReleaseStringUTFChars(env, device_name, name); + (*env)->ReleaseStringUTFChars(env, device_desc, desc); +#endif // SDL_JOYSTICK_ANDROID +} + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick)( + JNIEnv *env, jclass jcls, + jint device_id) +{ +#ifdef SDL_JOYSTICK_ANDROID + Android_RemoveJoystick(device_id); +#endif // SDL_JOYSTICK_ANDROID +} + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddHaptic)( + JNIEnv *env, jclass jcls, jint device_id, jstring device_name) +{ +#ifdef SDL_HAPTIC_ANDROID + const char *name = (*env)->GetStringUTFChars(env, device_name, NULL); + + Android_AddHaptic(device_id, name); + + (*env)->ReleaseStringUTFChars(env, device_name, name); +#endif // SDL_HAPTIC_ANDROID +} + +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic)( + JNIEnv *env, jclass jcls, jint device_id) +{ +#ifdef SDL_HAPTIC_ANDROID + Android_RemoveHaptic(device_id); +#endif +} + +// Called from surfaceCreated() +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceCreated)(JNIEnv *env, jclass jcls) +{ + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + SDL_WindowData *data = Android_Window->internal; + + data->native_window = Android_JNI_GetNativeWindow(); + SDL_SetPointerProperty(SDL_GetWindowProperties(Android_Window), SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER, data->native_window); + if (data->native_window == NULL) { + SDL_SetError("Could not fetch native window from UI thread"); + } + } + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Called from surfaceChanged() +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceChanged)(JNIEnv *env, jclass jcls) +{ + SDL_LockMutex(Android_ActivityMutex); + +#ifdef SDL_VIDEO_OPENGL_EGL + if (Android_Window && (Android_Window->flags & SDL_WINDOW_OPENGL)) { + SDL_VideoDevice *_this = SDL_GetVideoDevice(); + SDL_WindowData *data = Android_Window->internal; + + // If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here + if (data->egl_surface == EGL_NO_SURFACE) { + data->egl_surface = SDL_EGL_CreateSurface(_this, Android_Window, (NativeWindowType)data->native_window); + SDL_SetPointerProperty(SDL_GetWindowProperties(Android_Window), SDL_PROP_WINDOW_ANDROID_SURFACE_POINTER, data->egl_surface); + } + + // GL Context handling is done in the event loop because this function is run from the Java thread + } +#endif + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Called from surfaceDestroyed() +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceDestroyed)(JNIEnv *env, jclass jcls) +{ + int nb_attempt = 50; + +retry: + + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + SDL_WindowData *data = Android_Window->internal; + + // Wait for Main thread being paused and context un-activated to release 'egl_surface' + if ((Android_Window->flags & SDL_WINDOW_OPENGL) && !data->backup_done) { + nb_attempt -= 1; + if (nb_attempt == 0) { + SDL_SetError("Try to release egl_surface with context probably still active"); + } else { + SDL_UnlockMutex(Android_ActivityMutex); + SDL_Delay(10); + goto retry; + } + } + +#ifdef SDL_VIDEO_OPENGL_EGL + if (data->egl_surface != EGL_NO_SURFACE) { + SDL_EGL_DestroySurface(SDL_GetVideoDevice(), data->egl_surface); + data->egl_surface = EGL_NO_SURFACE; + } +#endif + + if (data->native_window) { + ANativeWindow_release(data->native_window); + data->native_window = NULL; + } + + // GL Context handling is done in the event loop because this function is run from the Java thread + } + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Keydown +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyDown)( + JNIEnv *env, jclass jcls, + jint keycode) +{ + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + Android_OnKeyDown(keycode); + } + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Keyup +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyUp)( + JNIEnv *env, jclass jcls, + jint keycode) +{ + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + Android_OnKeyUp(keycode); + } + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Virtual keyboard return key might stop text input +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(onNativeSoftReturnKey)( + JNIEnv *env, jclass jcls) +{ + if (SDL_GetHintBoolean(SDL_HINT_RETURN_KEY_HIDES_IME, false)) { + SDL_StopTextInput(Android_Window); + return JNI_TRUE; + } + return JNI_FALSE; +} + +// Keyboard Focus Lost +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyboardFocusLost)( + JNIEnv *env, jclass jcls) +{ + // Calling SDL_StopTextInput will take care of hiding the keyboard and cleaning up the DummyText widget + SDL_StopTextInput(Android_Window); +} + +// Touch +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeTouch)( + JNIEnv *env, jclass jcls, + jint touch_device_id_in, jint pointer_finger_id_in, + jint action, jfloat x, jfloat y, jfloat p) +{ + SDL_LockMutex(Android_ActivityMutex); + + Android_OnTouch(Android_Window, touch_device_id_in, pointer_finger_id_in, action, x, y, p); + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Mouse +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeMouse)( + JNIEnv *env, jclass jcls, + jint button, jint action, jfloat x, jfloat y, jboolean relative) +{ + SDL_LockMutex(Android_ActivityMutex); + + Android_OnMouse(Android_Window, button, action, x, y, relative); + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Pen +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePen)( + JNIEnv *env, jclass jcls, + jint pen_id_in, jint button, jint action, jfloat x, jfloat y, jfloat p) +{ + SDL_LockMutex(Android_ActivityMutex); + + Android_OnPen(Android_Window, pen_id_in, button, action, x, y, p); + + SDL_UnlockMutex(Android_ActivityMutex); +} + +// Accelerometer +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeAccel)( + JNIEnv *env, jclass jcls, + jfloat x, jfloat y, jfloat z) +{ + fLastAccelerometer[0] = x; + fLastAccelerometer[1] = y; + fLastAccelerometer[2] = z; + bHasNewData = true; +} + +// Clipboard +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeClipboardChanged)( + JNIEnv *env, jclass jcls) +{ + // TODO: compute new mime types + SDL_SendClipboardUpdate(false, NULL, 0); +} + +// Low memory +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeLowMemory)( + JNIEnv *env, jclass cls) +{ + Android_SendLifecycleEvent(SDL_ANDROID_LIFECYCLE_LOWMEMORY); +} + +/* Locale + * requires android:configChanges="layoutDirection|locale" in AndroidManifest.xml */ +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeLocaleChanged)( + JNIEnv *env, jclass cls) +{ + SDL_SendAppEvent(SDL_EVENT_LOCALE_CHANGED); +} + +// Dark mode +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDarkModeChanged)( + JNIEnv *env, jclass cls, jboolean enabled) +{ + Android_SetDarkMode(enabled); +} + +// Send Quit event to "SDLThread" thread +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSendQuit)( + JNIEnv *env, jclass cls) +{ + Android_SendLifecycleEvent(SDL_ANDROID_LIFECYCLE_DESTROY); +} + +// Activity ends +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeQuit)( + JNIEnv *env, jclass cls) +{ + const char *str; + + if (Android_ActivityMutex) { + SDL_DestroyMutex(Android_ActivityMutex); + Android_ActivityMutex = NULL; + } + + if (Android_LifecycleMutex) { + SDL_DestroyMutex(Android_LifecycleMutex); + Android_LifecycleMutex = NULL; + } + + if (Android_LifecycleEventSem) { + SDL_DestroySemaphore(Android_LifecycleEventSem); + Android_LifecycleEventSem = NULL; + } + + Android_NumLifecycleEvents = 0; + + Internal_Android_Destroy_AssetManager(); + + str = SDL_GetError(); + if (str && str[0]) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "SDLActivity thread ends (error=%s)", str); + } else { + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "SDLActivity thread ends"); + } +} + +// Pause +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePause)( + JNIEnv *env, jclass cls) +{ + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativePause()"); + + Android_SendLifecycleEvent(SDL_ANDROID_LIFECYCLE_PAUSE); +} + +// Resume +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeResume)( + JNIEnv *env, jclass cls) +{ + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeResume()"); + + Android_SendLifecycleEvent(SDL_ANDROID_LIFECYCLE_RESUME); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeFocusChanged)( + JNIEnv *env, jclass cls, jboolean hasFocus) +{ + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeFocusChanged()"); + SDL_SendWindowEvent(Android_Window, (hasFocus ? SDL_EVENT_WINDOW_FOCUS_GAINED : SDL_EVENT_WINDOW_FOCUS_LOST), 0, 0); + } + + SDL_UnlockMutex(Android_ActivityMutex); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE_INPUT_CONNECTION(nativeCommitText)( + JNIEnv *env, jclass cls, + jstring text, jint newCursorPosition) +{ + const char *utftext = (*env)->GetStringUTFChars(env, text, NULL); + + SDL_SendKeyboardText(utftext); + + (*env)->ReleaseStringUTFChars(env, text, utftext); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE_INPUT_CONNECTION(nativeGenerateScancodeForUnichar)( + JNIEnv *env, jclass cls, + jchar chUnicode) +{ + SDL_SendKeyboardUnicodeKey(0, chUnicode); +} + +JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetHint)( + JNIEnv *env, jclass cls, + jstring name) +{ + const char *utfname = (*env)->GetStringUTFChars(env, name, NULL); + const char *hint = SDL_GetHint(utfname); + + jstring result = (*env)->NewStringUTF(env, hint); + (*env)->ReleaseStringUTFChars(env, name, utfname); + + return result; +} + +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeGetHintBoolean)( + JNIEnv *env, jclass cls, + jstring name, jboolean default_value) +{ + jboolean result; + + const char *utfname = (*env)->GetStringUTFChars(env, name, NULL); + result = SDL_GetHintBoolean(utfname, default_value); + (*env)->ReleaseStringUTFChars(env, name, utfname); + + return result; +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetenv)( + JNIEnv *env, jclass cls, + jstring name, jstring value) +{ + const char *utfname = (*env)->GetStringUTFChars(env, name, NULL); + const char *utfvalue = (*env)->GetStringUTFChars(env, value, NULL); + + // This is only called at startup, to initialize the environment + // Note that we call setenv() directly to avoid affecting SDL environments + setenv(utfname, utfvalue, 1); // This should NOT be SDL_setenv() + + (*env)->ReleaseStringUTFChars(env, name, utfname); + (*env)->ReleaseStringUTFChars(env, value, utfvalue); +} + +/******************************************************************************* + Functions called by SDL into Java +*******************************************************************************/ + +static SDL_AtomicInt s_active; +struct LocalReferenceHolder +{ + JNIEnv *m_env; + const char *m_func; +}; + +static struct LocalReferenceHolder LocalReferenceHolder_Setup(const char *func) +{ + struct LocalReferenceHolder refholder; + refholder.m_env = NULL; + refholder.m_func = func; +#ifdef DEBUG_JNI + SDL_Log("Entering function %s", func); +#endif + return refholder; +} + +static bool LocalReferenceHolder_Init(struct LocalReferenceHolder *refholder, JNIEnv *env) +{ + const int capacity = 16; + if ((*env)->PushLocalFrame(env, capacity) < 0) { + SDL_SetError("Failed to allocate enough JVM local references"); + return false; + } + SDL_AtomicIncRef(&s_active); + refholder->m_env = env; + return true; +} + +static void LocalReferenceHolder_Cleanup(struct LocalReferenceHolder *refholder) +{ +#ifdef DEBUG_JNI + SDL_Log("Leaving function %s", refholder->m_func); +#endif + if (refholder->m_env) { + JNIEnv *env = refholder->m_env; + (*env)->PopLocalFrame(env, NULL); + SDL_AtomicDecRef(&s_active); + } +} + +ANativeWindow *Android_JNI_GetNativeWindow(void) +{ + ANativeWindow *anw = NULL; + jobject s; + JNIEnv *env = Android_JNI_GetEnv(); + + s = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetNativeSurface); + if (s) { + anw = ANativeWindow_fromSurface(env, s); + (*env)->DeleteLocalRef(env, s); + } + + return anw; +} + +void Android_JNI_SetActivityTitle(const char *title) +{ + JNIEnv *env = Android_JNI_GetEnv(); + + jstring jtitle = (*env)->NewStringUTF(env, title); + (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetActivityTitle, jtitle); + (*env)->DeleteLocalRef(env, jtitle); +} + +void Android_JNI_SetWindowStyle(bool fullscreen) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mActivityClass, midSetWindowStyle, fullscreen ? 1 : 0); +} + +void Android_JNI_SetOrientation(int w, int h, int resizable, const char *hint) +{ + JNIEnv *env = Android_JNI_GetEnv(); + + jstring jhint = (*env)->NewStringUTF(env, (hint ? hint : "")); + (*env)->CallStaticVoidMethod(env, mActivityClass, midSetOrientation, w, h, (resizable ? 1 : 0), jhint); + (*env)->DeleteLocalRef(env, jhint); +} + +SDL_DisplayOrientation Android_JNI_GetDisplayNaturalOrientation(void) +{ + return displayNaturalOrientation; +} + +SDL_DisplayOrientation Android_JNI_GetDisplayCurrentOrientation(void) +{ + return displayCurrentOrientation; +} + +void Android_JNI_MinizeWindow(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mActivityClass, midMinimizeWindow); +} + +bool Android_JNI_ShouldMinimizeOnFocusLoss(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midShouldMinimizeOnFocusLoss); +} + +bool Android_JNI_GetAccelerometerValues(float values[3]) +{ + bool result = false; + + if (bHasNewData) { + int i; + for (i = 0; i < 3; ++i) { + values[i] = fLastAccelerometer[i]; + } + bHasNewData = false; + result = true; + } + + return result; +} + +/* + * Audio support + */ +void Android_StartAudioHotplug(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) +{ + JNIEnv *env = Android_JNI_GetEnv(); + // this will fire the callback for each existing device right away (which will eventually SDL_AddAudioDevice), and again later when things change. + (*env)->CallStaticVoidMethod(env, mAudioManagerClass, midRegisterAudioDeviceCallback); + *default_playback = *default_recording = NULL; // !!! FIXME: how do you decide the default device id? +} + +void Android_StopAudioHotplug(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mAudioManagerClass, midUnregisterAudioDeviceCallback); +} + +static void Android_JNI_AudioSetThreadPriority(int recording, int device_id) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mAudioManagerClass, midAudioSetThreadPriority, recording, device_id); +} + +void Android_AudioThreadInit(SDL_AudioDevice *device) +{ + Android_JNI_AudioSetThreadPriority((int) device->recording, (int)device->instance_id); +} + +// Test for an exception and call SDL_SetError with its detail if one occurs +// If the parameter silent is truthy then SDL_SetError() will not be called. +static bool Android_JNI_ExceptionOccurred(bool silent) +{ + JNIEnv *env = Android_JNI_GetEnv(); + jthrowable exception; + + // Detect mismatch LocalReferenceHolder_Init/Cleanup + SDL_assert(SDL_GetAtomicInt(&s_active) > 0); + + exception = (*env)->ExceptionOccurred(env); + if (exception != NULL) { + jmethodID mid; + + // Until this happens most JNI operations have undefined behaviour + (*env)->ExceptionClear(env); + + if (!silent) { + jclass exceptionClass = (*env)->GetObjectClass(env, exception); + jclass classClass = (*env)->FindClass(env, "java/lang/Class"); + jstring exceptionName; + const char *exceptionNameUTF8; + jstring exceptionMessage; + + mid = (*env)->GetMethodID(env, classClass, "getName", "()Ljava/lang/String;"); + exceptionName = (jstring)(*env)->CallObjectMethod(env, exceptionClass, mid); + exceptionNameUTF8 = (*env)->GetStringUTFChars(env, exceptionName, 0); + + mid = (*env)->GetMethodID(env, exceptionClass, "getMessage", "()Ljava/lang/String;"); + exceptionMessage = (jstring)(*env)->CallObjectMethod(env, exception, mid); + + if (exceptionMessage != NULL) { + const char *exceptionMessageUTF8 = (*env)->GetStringUTFChars(env, exceptionMessage, 0); + SDL_SetError("%s: %s", exceptionNameUTF8, exceptionMessageUTF8); + (*env)->ReleaseStringUTFChars(env, exceptionMessage, exceptionMessageUTF8); + } else { + SDL_SetError("%s", exceptionNameUTF8); + } + + (*env)->ReleaseStringUTFChars(env, exceptionName, exceptionNameUTF8); + } + + return true; + } + + return false; +} + +static void Internal_Android_Create_AssetManager(void) +{ + + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(__FUNCTION__); + JNIEnv *env = Android_JNI_GetEnv(); + jmethodID mid; + jobject context; + jobject javaAssetManager; + + if (!LocalReferenceHolder_Init(&refs, env)) { + LocalReferenceHolder_Cleanup(&refs); + return; + } + + // context = SDLActivity.getContext(); + context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); + + // javaAssetManager = context.getAssets(); + mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), + "getAssets", "()Landroid/content/res/AssetManager;"); + javaAssetManager = (*env)->CallObjectMethod(env, context, mid); + + /** + * Given a Dalvik AssetManager object, obtain the corresponding native AAssetManager + * object. Note that the caller is responsible for obtaining and holding a VM reference + * to the jobject to prevent its being garbage collected while the native object is + * in use. + */ + javaAssetManagerRef = (*env)->NewGlobalRef(env, javaAssetManager); + asset_manager = AAssetManager_fromJava(env, javaAssetManagerRef); + + if (!asset_manager) { + (*env)->DeleteGlobalRef(env, javaAssetManagerRef); + Android_JNI_ExceptionOccurred(true); + } + + LocalReferenceHolder_Cleanup(&refs); +} + +static void Internal_Android_Destroy_AssetManager(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + + if (asset_manager) { + (*env)->DeleteGlobalRef(env, javaAssetManagerRef); + asset_manager = NULL; + } +} + +bool Android_JNI_FileOpen(void **puserdata, const char *fileName, const char *mode) +{ + SDL_assert(puserdata != NULL); + + AAsset *asset = NULL; + *puserdata = NULL; + + if (!asset_manager) { + Internal_Android_Create_AssetManager(); + } + + if (!asset_manager) { + return SDL_SetError("Couldn't create asset manager"); + } + + asset = AAssetManager_open(asset_manager, fileName, AASSET_MODE_UNKNOWN); + if (!asset) { + return SDL_SetError("Couldn't open asset '%s'", fileName); + } + + *puserdata = (void *)asset; + return true; +} + +size_t Android_JNI_FileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status) +{ + const int bytes = AAsset_read((AAsset *)userdata, buffer, size); + if (bytes < 0) { + SDL_SetError("AAsset_read() failed"); + return 0; + } + return (size_t)bytes; +} + +size_t Android_JNI_FileWrite(void *userdata, const void *buffer, size_t size, SDL_IOStatus *status) +{ + SDL_SetError("Cannot write to Android package filesystem"); + return 0; +} + +Sint64 Android_JNI_FileSize(void *userdata) +{ + return (Sint64) AAsset_getLength64((AAsset *)userdata); +} + +Sint64 Android_JNI_FileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence) +{ + return (Sint64) AAsset_seek64((AAsset *)userdata, offset, (int)whence); +} + +bool Android_JNI_FileClose(void *userdata) +{ + AAsset_close((AAsset *)userdata); + return true; +} + +bool Android_JNI_SetClipboardText(const char *text) +{ + JNIEnv *env = Android_JNI_GetEnv(); + jstring string = (*env)->NewStringUTF(env, text); + (*env)->CallStaticVoidMethod(env, mActivityClass, midClipboardSetText, string); + (*env)->DeleteLocalRef(env, string); + return true; +} + +char *Android_JNI_GetClipboardText(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + char *text = NULL; + jstring string; + + string = (*env)->CallStaticObjectMethod(env, mActivityClass, midClipboardGetText); + if (string) { + const char *utf = (*env)->GetStringUTFChars(env, string, 0); + if (utf) { + text = SDL_strdup(utf); + (*env)->ReleaseStringUTFChars(env, string, utf); + } + (*env)->DeleteLocalRef(env, string); + } + + return (!text) ? SDL_strdup("") : text; +} + +bool Android_JNI_HasClipboardText(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midClipboardHasText); +} + +/* returns 0 on success or -1 on error (others undefined then) + * returns truthy or falsy value in plugged, charged and battery + * returns the value in seconds and percent or -1 if not available + */ +int Android_JNI_GetPowerInfo(int *plugged, int *charged, int *battery, int *seconds, int *percent) +{ + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(__FUNCTION__); + JNIEnv *env = Android_JNI_GetEnv(); + jmethodID mid; + jobject context; + jstring action; + jclass cls; + jobject filter; + jobject intent; + jstring iname; + jmethodID imid; + jstring bname; + jmethodID bmid; + if (!LocalReferenceHolder_Init(&refs, env)) { + LocalReferenceHolder_Cleanup(&refs); + return -1; + } + + // context = SDLActivity.getContext(); + context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); + + action = (*env)->NewStringUTF(env, "android.intent.action.BATTERY_CHANGED"); + + cls = (*env)->FindClass(env, "android/content/IntentFilter"); + + mid = (*env)->GetMethodID(env, cls, "", "(Ljava/lang/String;)V"); + filter = (*env)->NewObject(env, cls, mid, action); + + (*env)->DeleteLocalRef(env, action); + + mid = (*env)->GetMethodID(env, mActivityClass, "registerReceiver", "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;"); + intent = (*env)->CallObjectMethod(env, context, mid, NULL, filter); + + (*env)->DeleteLocalRef(env, filter); + + cls = (*env)->GetObjectClass(env, intent); + + imid = (*env)->GetMethodID(env, cls, "getIntExtra", "(Ljava/lang/String;I)I"); + + // Watch out for C89 scoping rules because of the macro +#define GET_INT_EXTRA(var, key) \ + int var; \ + iname = (*env)->NewStringUTF(env, key); \ + (var) = (*env)->CallIntMethod(env, intent, imid, iname, -1); \ + (*env)->DeleteLocalRef(env, iname); + + bmid = (*env)->GetMethodID(env, cls, "getBooleanExtra", "(Ljava/lang/String;Z)Z"); + + // Watch out for C89 scoping rules because of the macro +#define GET_BOOL_EXTRA(var, key) \ + int var; \ + bname = (*env)->NewStringUTF(env, key); \ + (var) = (*env)->CallBooleanMethod(env, intent, bmid, bname, JNI_FALSE); \ + (*env)->DeleteLocalRef(env, bname); + + if (plugged) { + // Watch out for C89 scoping rules because of the macro + GET_INT_EXTRA(plug, "plugged") // == BatteryManager.EXTRA_PLUGGED (API 5) + if (plug == -1) { + LocalReferenceHolder_Cleanup(&refs); + return -1; + } + // 1 == BatteryManager.BATTERY_PLUGGED_AC + // 2 == BatteryManager.BATTERY_PLUGGED_USB + *plugged = (0 < plug) ? 1 : 0; + } + + if (charged) { + // Watch out for C89 scoping rules because of the macro + GET_INT_EXTRA(status, "status") // == BatteryManager.EXTRA_STATUS (API 5) + if (status == -1) { + LocalReferenceHolder_Cleanup(&refs); + return -1; + } + // 5 == BatteryManager.BATTERY_STATUS_FULL + *charged = (status == 5) ? 1 : 0; + } + + if (battery) { + GET_BOOL_EXTRA(present, "present") // == BatteryManager.EXTRA_PRESENT (API 5) + *battery = present ? 1 : 0; + } + + if (seconds) { + *seconds = -1; // not possible + } + + if (percent) { + int level; + int scale; + + // Watch out for C89 scoping rules because of the macro + { + GET_INT_EXTRA(level_temp, "level") // == BatteryManager.EXTRA_LEVEL (API 5) + level = level_temp; + } + // Watch out for C89 scoping rules because of the macro + { + GET_INT_EXTRA(scale_temp, "scale") // == BatteryManager.EXTRA_SCALE (API 5) + scale = scale_temp; + } + + if ((level == -1) || (scale == -1)) { + LocalReferenceHolder_Cleanup(&refs); + return -1; + } + *percent = level * 100 / scale; + } + + (*env)->DeleteLocalRef(env, intent); + + LocalReferenceHolder_Cleanup(&refs); + return 0; +} + +// Add all touch devices +void Android_JNI_InitTouch(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mActivityClass, midInitTouch); +} + +void Android_JNI_PollInputDevices(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midPollInputDevices); +} + +void Android_JNI_PollHapticDevices(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midPollHapticDevices); +} + +void Android_JNI_HapticRun(int device_id, float intensity, int length) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticRun, device_id, intensity, length); +} + +void Android_JNI_HapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticRumble, device_id, low_frequency_intensity, high_frequency_intensity, length); +} + +void Android_JNI_HapticStop(int device_id) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticStop, device_id); +} + +// See SDLActivity.java for constants. +#define COMMAND_SET_KEEP_SCREEN_ON 5 + +bool SDL_SendAndroidMessage(Uint32 command, int param) +{ + if (command < 0x8000) { + return SDL_InvalidParamError("command"); + } + return Android_JNI_SendMessage(command, param); +} + +// sends message to be handled on the UI event dispatch thread +bool Android_JNI_SendMessage(int command, int param) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSendMessage, command, param); +} + +bool Android_JNI_SuspendScreenSaver(bool suspend) +{ + return Android_JNI_SendMessage(COMMAND_SET_KEEP_SCREEN_ON, (suspend == false) ? 0 : 1); +} + +void Android_JNI_ShowScreenKeyboard(int input_type, SDL_Rect *inputRect) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticBooleanMethod(env, mActivityClass, midShowTextInput, + input_type, + inputRect->x, + inputRect->y, + inputRect->w, + inputRect->h); +} + +void Android_JNI_HideScreenKeyboard(void) +{ + // has to match Activity constant + const int COMMAND_TEXTEDIT_HIDE = 3; + Android_JNI_SendMessage(COMMAND_TEXTEDIT_HIDE, 0); +} + +bool Android_JNI_IsScreenKeyboardShown(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + jboolean is_shown = 0; + is_shown = (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsScreenKeyboardShown); + return is_shown; +} + +bool Android_JNI_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID) +{ + JNIEnv *env; + jclass clazz; + jmethodID mid; + jobject context; + jstring title; + jstring message; + jintArray button_flags; + jintArray button_ids; + jobjectArray button_texts; + jintArray colors; + jobject text; + jint temp; + int i; + + env = Android_JNI_GetEnv(); + + // convert parameters + + clazz = (*env)->FindClass(env, "java/lang/String"); + + title = (*env)->NewStringUTF(env, messageboxdata->title); + message = (*env)->NewStringUTF(env, messageboxdata->message); + + button_flags = (*env)->NewIntArray(env, messageboxdata->numbuttons); + button_ids = (*env)->NewIntArray(env, messageboxdata->numbuttons); + button_texts = (*env)->NewObjectArray(env, messageboxdata->numbuttons, + clazz, NULL); + for (i = 0; i < messageboxdata->numbuttons; ++i) { + const SDL_MessageBoxButtonData *sdlButton; + + if (messageboxdata->flags & SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT) { + sdlButton = &messageboxdata->buttons[messageboxdata->numbuttons - 1 - i]; + } else { + sdlButton = &messageboxdata->buttons[i]; + } + + temp = sdlButton->flags; + (*env)->SetIntArrayRegion(env, button_flags, i, 1, &temp); + temp = sdlButton->buttonID; + (*env)->SetIntArrayRegion(env, button_ids, i, 1, &temp); + text = (*env)->NewStringUTF(env, sdlButton->text); + (*env)->SetObjectArrayElement(env, button_texts, i, text); + (*env)->DeleteLocalRef(env, text); + } + + if (messageboxdata->colorScheme) { + colors = (*env)->NewIntArray(env, SDL_MESSAGEBOX_COLOR_COUNT); + for (i = 0; i < SDL_MESSAGEBOX_COLOR_COUNT; ++i) { + temp = (0xFFU << 24) | + (messageboxdata->colorScheme->colors[i].r << 16) | + (messageboxdata->colorScheme->colors[i].g << 8) | + (messageboxdata->colorScheme->colors[i].b << 0); + (*env)->SetIntArrayRegion(env, colors, i, 1, &temp); + } + } else { + colors = NULL; + } + + (*env)->DeleteLocalRef(env, clazz); + + // context = SDLActivity.getContext(); + context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); + + clazz = (*env)->GetObjectClass(env, context); + + mid = (*env)->GetMethodID(env, clazz, + "messageboxShowMessageBox", "(ILjava/lang/String;Ljava/lang/String;[I[I[Ljava/lang/String;[I)I"); + *buttonID = (*env)->CallIntMethod(env, context, mid, + messageboxdata->flags, + title, + message, + button_flags, + button_ids, + button_texts, + colors); + + (*env)->DeleteLocalRef(env, context); + (*env)->DeleteLocalRef(env, clazz); + + // delete parameters + + (*env)->DeleteLocalRef(env, title); + (*env)->DeleteLocalRef(env, message); + (*env)->DeleteLocalRef(env, button_flags); + (*env)->DeleteLocalRef(env, button_ids); + (*env)->DeleteLocalRef(env, button_texts); + (*env)->DeleteLocalRef(env, colors); + + return true; +} + +/* +////////////////////////////////////////////////////////////////////////////// +// +// Functions exposed to SDL applications in SDL_system.h +////////////////////////////////////////////////////////////////////////////// +*/ + +void *SDL_GetAndroidJNIEnv(void) +{ + return Android_JNI_GetEnv(); +} + +void *SDL_GetAndroidActivity(void) +{ + // See SDL_system.h for caveats on using this function. + + JNIEnv *env = Android_JNI_GetEnv(); + if (!env) { + return NULL; + } + + // return SDLActivity.getContext(); + return (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); +} + +int SDL_GetAndroidSDKVersion(void) +{ + static int sdk_version; + if (!sdk_version) { + char sdk[PROP_VALUE_MAX] = { 0 }; + if (__system_property_get("ro.build.version.sdk", sdk) != 0) { + sdk_version = SDL_atoi(sdk); + } + } + return sdk_version; +} + +bool SDL_IsAndroidTablet(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsTablet); +} + +bool SDL_IsAndroidTV(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsAndroidTV); +} + +bool SDL_IsChromebook(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsChromebook); +} + +bool SDL_IsDeXMode(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsDeXMode); +} + +void SDL_SendAndroidBackButton(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mActivityClass, midManualBackButton); +} + +const char *SDL_GetAndroidInternalStoragePath(void) +{ + static char *s_AndroidInternalFilesPath = NULL; + + if (!s_AndroidInternalFilesPath) { + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(__FUNCTION__); + jmethodID mid; + jobject context; + jobject fileObject; + jstring pathString; + const char *path; + + JNIEnv *env = Android_JNI_GetEnv(); + if (!LocalReferenceHolder_Init(&refs, env)) { + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } + + // context = SDLActivity.getContext(); + context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); + if (!context) { + SDL_SetError("Couldn't get Android context!"); + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } + + // fileObj = context.getFilesDir(); + mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), + "getFilesDir", "()Ljava/io/File;"); + fileObject = (*env)->CallObjectMethod(env, context, mid); + if (!fileObject) { + SDL_SetError("Couldn't get internal directory"); + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } + + // path = fileObject.getCanonicalPath(); + mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, fileObject), + "getCanonicalPath", "()Ljava/lang/String;"); + pathString = (jstring)(*env)->CallObjectMethod(env, fileObject, mid); + if (Android_JNI_ExceptionOccurred(false)) { + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } + + path = (*env)->GetStringUTFChars(env, pathString, NULL); + s_AndroidInternalFilesPath = SDL_strdup(path); + (*env)->ReleaseStringUTFChars(env, pathString, path); + + LocalReferenceHolder_Cleanup(&refs); + } + return s_AndroidInternalFilesPath; +} + +Uint32 SDL_GetAndroidExternalStorageState(void) +{ + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(__FUNCTION__); + jmethodID mid; + jclass cls; + jstring stateString; + const char *state_string; + Uint32 stateFlags; + + JNIEnv *env = Android_JNI_GetEnv(); + if (!LocalReferenceHolder_Init(&refs, env)) { + LocalReferenceHolder_Cleanup(&refs); + return 0; + } + + cls = (*env)->FindClass(env, "android/os/Environment"); + mid = (*env)->GetStaticMethodID(env, cls, + "getExternalStorageState", "()Ljava/lang/String;"); + stateString = (jstring)(*env)->CallStaticObjectMethod(env, cls, mid); + + state_string = (*env)->GetStringUTFChars(env, stateString, NULL); + + // Print an info message so people debugging know the storage state + __android_log_print(ANDROID_LOG_INFO, "SDL", "external storage state: %s", state_string); + + if (SDL_strcmp(state_string, "mounted") == 0) { + stateFlags = SDL_ANDROID_EXTERNAL_STORAGE_READ | + SDL_ANDROID_EXTERNAL_STORAGE_WRITE; + } else if (SDL_strcmp(state_string, "mounted_ro") == 0) { + stateFlags = SDL_ANDROID_EXTERNAL_STORAGE_READ; + } else { + stateFlags = 0; + } + (*env)->ReleaseStringUTFChars(env, stateString, state_string); + + LocalReferenceHolder_Cleanup(&refs); + + return stateFlags; +} + +const char *SDL_GetAndroidExternalStoragePath(void) +{ + static char *s_AndroidExternalFilesPath = NULL; + + if (!s_AndroidExternalFilesPath) { + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(__FUNCTION__); + jmethodID mid; + jobject context; + jobject fileObject; + jstring pathString; + const char *path; + + JNIEnv *env = Android_JNI_GetEnv(); + if (!LocalReferenceHolder_Init(&refs, env)) { + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } + + // context = SDLActivity.getContext(); + context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); + + // fileObj = context.getExternalFilesDir(); + mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), + "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;"); + fileObject = (*env)->CallObjectMethod(env, context, mid, NULL); + if (!fileObject) { + SDL_SetError("Couldn't get external directory"); + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } + + // path = fileObject.getAbsolutePath(); + mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, fileObject), + "getAbsolutePath", "()Ljava/lang/String;"); + pathString = (jstring)(*env)->CallObjectMethod(env, fileObject, mid); + + path = (*env)->GetStringUTFChars(env, pathString, NULL); + s_AndroidExternalFilesPath = SDL_strdup(path); + (*env)->ReleaseStringUTFChars(env, pathString, path); + + LocalReferenceHolder_Cleanup(&refs); + } + return s_AndroidExternalFilesPath; +} + +const char *SDL_GetAndroidCachePath(void) +{ + // !!! FIXME: lots of duplication with SDL_GetAndroidExternalStoragePath and SDL_GetAndroidInternalStoragePath; consolidate these functions! + static char *s_AndroidCachePath = NULL; + + if (!s_AndroidCachePath) { + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(__FUNCTION__); + jmethodID mid; + jobject context; + jobject fileObject; + jstring pathString; + const char *path; + + JNIEnv *env = Android_JNI_GetEnv(); + if (!LocalReferenceHolder_Init(&refs, env)) { + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } + + // context = SDLActivity.getContext(); + context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); + + // fileObj = context.getExternalFilesDir(); + mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), + "getCacheDir", "()Ljava/io/File;"); + fileObject = (*env)->CallObjectMethod(env, context, mid, NULL); + if (!fileObject) { + SDL_SetError("Couldn't get cache directory"); + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } + + // path = fileObject.getAbsolutePath(); + mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, fileObject), + "getAbsolutePath", "()Ljava/lang/String;"); + pathString = (jstring)(*env)->CallObjectMethod(env, fileObject, mid); + + path = (*env)->GetStringUTFChars(env, pathString, NULL); + s_AndroidCachePath = SDL_strdup(path); + (*env)->ReleaseStringUTFChars(env, pathString, path); + + LocalReferenceHolder_Cleanup(&refs); + } + return s_AndroidCachePath; +} + +bool SDL_ShowAndroidToast(const char *message, int duration, int gravity, int xOffset, int yOffset) +{ + return Android_JNI_ShowToast(message, duration, gravity, xOffset, yOffset); +} + +void Android_JNI_GetManifestEnvironmentVariables(void) +{ + if (!mActivityClass || !midGetManifestEnvironmentVariables) { + __android_log_print(ANDROID_LOG_WARN, "SDL", "Request to get environment variables before JNI is ready"); + return; + } + + if (!bHasEnvironmentVariables) { + JNIEnv *env = Android_JNI_GetEnv(); + bool ret = (*env)->CallStaticBooleanMethod(env, mActivityClass, midGetManifestEnvironmentVariables); + if (ret) { + bHasEnvironmentVariables = true; + } + } +} + +int Android_JNI_CreateCustomCursor(SDL_Surface *surface, int hot_x, int hot_y) +{ + JNIEnv *env = Android_JNI_GetEnv(); + int custom_cursor = 0; + jintArray pixels; + pixels = (*env)->NewIntArray(env, surface->w * surface->h); + if (pixels) { + (*env)->SetIntArrayRegion(env, pixels, 0, surface->w * surface->h, (int *)surface->pixels); + custom_cursor = (*env)->CallStaticIntMethod(env, mActivityClass, midCreateCustomCursor, pixels, surface->w, surface->h, hot_x, hot_y); + (*env)->DeleteLocalRef(env, pixels); + } else { + SDL_OutOfMemory(); + } + return custom_cursor; +} + +void Android_JNI_DestroyCustomCursor(int cursorID) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mActivityClass, midDestroyCustomCursor, cursorID); +} + +bool Android_JNI_SetCustomCursor(int cursorID) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetCustomCursor, cursorID); +} + +bool Android_JNI_SetSystemCursor(int cursorID) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetSystemCursor, cursorID); +} + +bool Android_JNI_SupportsRelativeMouse(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSupportsRelativeMouse); +} + +bool Android_JNI_SetRelativeMouseEnabled(bool enabled) +{ + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetRelativeMouseEnabled, (enabled == 1)); +} + +typedef struct NativePermissionRequestInfo +{ + int request_code; + char *permission; + SDL_RequestAndroidPermissionCallback callback; + void *userdata; + struct NativePermissionRequestInfo *next; +} NativePermissionRequestInfo; + +static NativePermissionRequestInfo pending_permissions; + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePermissionResult)( + JNIEnv *env, jclass cls, + jint requestCode, jboolean result) +{ + SDL_LockMutex(Android_ActivityMutex); + NativePermissionRequestInfo *prev = &pending_permissions; + for (NativePermissionRequestInfo *info = prev->next; info != NULL; info = info->next) { + if (info->request_code == (int) requestCode) { + prev->next = info->next; + SDL_UnlockMutex(Android_ActivityMutex); + info->callback(info->userdata, info->permission, result ? true : false); + SDL_free(info->permission); + SDL_free(info); + return; + } + prev = info; + } + + SDL_UnlockMutex(Android_ActivityMutex); +} + +bool SDL_RequestAndroidPermission(const char *permission, SDL_RequestAndroidPermissionCallback cb, void *userdata) +{ + if (!permission) { + return SDL_InvalidParamError("permission"); + } else if (!cb) { + return SDL_InvalidParamError("cb"); + } + + NativePermissionRequestInfo *info = (NativePermissionRequestInfo *) SDL_calloc(1, sizeof (NativePermissionRequestInfo)); + if (!info) { + return false; + } + + info->permission = SDL_strdup(permission); + if (!info->permission) { + SDL_free(info); + return false; + } + + static SDL_AtomicInt next_request_code; + info->request_code = SDL_AddAtomicInt(&next_request_code, 1); + + info->callback = cb; + info->userdata = userdata; + + SDL_LockMutex(Android_ActivityMutex); + info->next = pending_permissions.next; + pending_permissions.next = info; + SDL_UnlockMutex(Android_ActivityMutex); + + JNIEnv *env = Android_JNI_GetEnv(); + jstring jpermission = (*env)->NewStringUTF(env, permission); + (*env)->CallStaticVoidMethod(env, mActivityClass, midRequestPermission, jpermission, info->request_code); + (*env)->DeleteLocalRef(env, jpermission); + + return true; +} + +// Show toast notification +bool Android_JNI_ShowToast(const char *message, int duration, int gravity, int xOffset, int yOffset) +{ + bool result; + JNIEnv *env = Android_JNI_GetEnv(); + jstring jmessage = (*env)->NewStringUTF(env, message); + result = (*env)->CallStaticBooleanMethod(env, mActivityClass, midShowToast, jmessage, duration, gravity, xOffset, yOffset); + (*env)->DeleteLocalRef(env, jmessage); + return result; +} + +bool Android_JNI_GetLocale(char *buf, size_t buflen) +{ + AConfiguration *cfg; + + SDL_assert(buflen > 6); + + // Need to re-create the asset manager if locale has changed (SDL_EVENT_LOCALE_CHANGED) + Internal_Android_Destroy_AssetManager(); + + if (!asset_manager) { + Internal_Android_Create_AssetManager(); + } + + if (!asset_manager) { + return false; + } + + cfg = AConfiguration_new(); + if (!cfg) { + return false; + } + + { + char language[2] = {}; + char country[2] = {}; + size_t id = 0; + + AConfiguration_fromAssetManager(cfg, asset_manager); + AConfiguration_getLanguage(cfg, language); + AConfiguration_getCountry(cfg, country); + + // Indonesian is "id" according to ISO 639.2, but on Android is "in" because of Java backwards compatibility + if (language[0] == 'i' && language[1] == 'n') { + language[1] = 'd'; + } + + // copy language (not null terminated) + if (language[0]) { + buf[id++] = language[0]; + if (language[1]) { + buf[id++] = language[1]; + } + } + + buf[id++] = '_'; + + // copy country (not null terminated) + if (country[0]) { + buf[id++] = country[0]; + if (country[1]) { + buf[id++] = country[1]; + } + } + + buf[id++] = '\0'; + SDL_assert(id <= buflen); + } + + AConfiguration_delete(cfg); + + return true; +} + +bool Android_JNI_OpenURL(const char *url) +{ + bool result; + JNIEnv *env = Android_JNI_GetEnv(); + jstring jurl = (*env)->NewStringUTF(env, url); + result = (*env)->CallStaticBooleanMethod(env, mActivityClass, midOpenURL, jurl); + (*env)->DeleteLocalRef(env, jurl); + return result; +} + +int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode) +{ + // Get fopen-style modes + int moderead = 0, modewrite = 0, modeappend = 0, modeupdate = 0; + + for (const char *cmode = mode; *cmode; cmode++) { + switch (*cmode) { + case 'a': + modeappend = 1; + break; + case 'r': + moderead = 1; + break; + case 'w': + modewrite = 1; + break; + case '+': + modeupdate = 1; + break; + default: + break; + } + } + + // Translate fopen-style modes to ContentResolver modes. + // Android only allows "r", "w", "wt", "wa", "rw" or "rwt". + const char *contentResolverMode = "r"; + + if (moderead) { + if (modewrite) { + contentResolverMode = "rwt"; + } else { + contentResolverMode = modeupdate ? "rw" : "r"; + } + } else if (modewrite) { + contentResolverMode = modeupdate ? "rwt" : "wt"; + } else if (modeappend) { + contentResolverMode = modeupdate ? "rw" : "wa"; + } + + JNIEnv *env = Android_JNI_GetEnv(); + jstring jstringUri = (*env)->NewStringUTF(env, uri); + jstring jstringMode = (*env)->NewStringUTF(env, contentResolverMode); + jint fd = (*env)->CallStaticIntMethod(env, mActivityClass, midOpenFileDescriptor, jstringUri, jstringMode); + (*env)->DeleteLocalRef(env, jstringUri); + (*env)->DeleteLocalRef(env, jstringMode); + + if (fd == -1) { + SDL_SetError("Unspecified error in JNI"); + } + + 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(count + 1, sizeof(char*)); + + if (charFileList == NULL) { + 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); + 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); + } +} + +bool Android_JNI_OpenFileDialog( + SDL_DialogFileCallback callback, void* userdata, + const SDL_DialogFileFilter *filters, int nfilters, bool forwrite, + bool multiple) +{ + if (mAndroidFileDialogData.callback != NULL) { + SDL_SetError("Only one file dialog can be run at a time."); + return false; + } + + if (forwrite) { + multiple = false; + } + + JNIEnv *env = Android_JNI_GetEnv(); + + // Setup filters + jobjectArray filtersArray = NULL; + if (filters) { + jclass stringClass = (*env)->FindClass(env, "java/lang/String"); + filtersArray = (*env)->NewObjectArray(env, nfilters, stringClass, NULL); + + // Convert to string + for (int i = 0; i < nfilters; 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_AddAtomicInt(&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_AddAtomicInt(&next_request_code, -1); + SDL_SetError("Unspecified error in JNI"); + + return false; + } + + return true; +} + +#endif // SDL_PLATFORM_ANDROID diff --git a/contrib/SDL-3.2.8/src/core/android/SDL_android.h b/contrib/SDL-3.2.8/src/core/android/SDL_android.h new file mode 100644 index 0000000..3541c2a --- /dev/null +++ b/contrib/SDL-3.2.8/src/core/android/SDL_android.h @@ -0,0 +1,163 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 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" + +#ifndef SDL_android_h +#define SDL_android_h + +// Set up for C function definitions, even when using C++ +#ifdef __cplusplus +/* *INDENT-OFF* */ +extern "C" { +/* *INDENT-ON* */ +#endif + +#include +#include + +#include "../../audio/SDL_sysaudio.h" + +// this appears to be broken right now (on Android, not SDL, I think...?). +#define ALLOW_MULTIPLE_ANDROID_AUDIO_DEVICES 0 + +// Life cycle +typedef enum +{ + SDL_ANDROID_LIFECYCLE_WAKE, + SDL_ANDROID_LIFECYCLE_PAUSE, + SDL_ANDROID_LIFECYCLE_RESUME, + SDL_ANDROID_LIFECYCLE_LOWMEMORY, + SDL_ANDROID_LIFECYCLE_DESTROY, + SDL_NUM_ANDROID_LIFECYCLE_EVENTS +} SDL_AndroidLifecycleEvent; + +void Android_SendLifecycleEvent(SDL_AndroidLifecycleEvent event); +bool Android_WaitLifecycleEvent(SDL_AndroidLifecycleEvent *event, Sint64 timeoutNS); + +void Android_LockActivityMutex(void); +void Android_UnlockActivityMutex(void); + +// Interface from the SDL library into the Android Java activity +extern void Android_JNI_SetActivityTitle(const char *title); +extern void Android_JNI_SetWindowStyle(bool fullscreen); +extern void Android_JNI_SetOrientation(int w, int h, int resizable, const char *hint); +extern void Android_JNI_MinizeWindow(void); +extern bool Android_JNI_ShouldMinimizeOnFocusLoss(void); + +extern bool Android_JNI_GetAccelerometerValues(float values[3]); +extern void Android_JNI_ShowScreenKeyboard(int input_type, SDL_Rect *inputRect); +extern void Android_JNI_HideScreenKeyboard(void); +extern bool Android_JNI_IsScreenKeyboardShown(void); +extern ANativeWindow *Android_JNI_GetNativeWindow(void); + +extern SDL_DisplayOrientation Android_JNI_GetDisplayNaturalOrientation(void); +extern SDL_DisplayOrientation Android_JNI_GetDisplayCurrentOrientation(void); + +// Audio support +void Android_StartAudioHotplug(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording); +void Android_StopAudioHotplug(void); +extern void Android_AudioThreadInit(SDL_AudioDevice *device); + +// Detecting device type +extern bool Android_IsDeXMode(void); +extern bool Android_IsChromebook(void); + +bool Android_JNI_FileOpen(void **puserdata, const char *fileName, const char *mode); +Sint64 Android_JNI_FileSize(void *userdata); +Sint64 Android_JNI_FileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence); +size_t Android_JNI_FileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status); +size_t Android_JNI_FileWrite(void *userdata, const void *buffer, size_t size, SDL_IOStatus *status); +bool Android_JNI_FileClose(void *userdata); + +// Environment support +void Android_JNI_GetManifestEnvironmentVariables(void); +int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode); + +// Clipboard support +bool Android_JNI_SetClipboardText(const char *text); +char *Android_JNI_GetClipboardText(void); +bool Android_JNI_HasClipboardText(void); + +// Power support +int Android_JNI_GetPowerInfo(int *plugged, int *charged, int *battery, int *seconds, int *percent); + +// Joystick support +void Android_JNI_PollInputDevices(void); + +// Haptic support +void Android_JNI_PollHapticDevices(void); +void Android_JNI_HapticRun(int device_id, float intensity, int length); +void Android_JNI_HapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length); +void Android_JNI_HapticStop(int device_id); + +// Video +bool Android_JNI_SuspendScreenSaver(bool suspend); + +// Touch support +void Android_JNI_InitTouch(void); + +// Threads +#include +JNIEnv *Android_JNI_GetEnv(void); +bool Android_JNI_SetupThread(void); + +// Locale +bool Android_JNI_GetLocale(char *buf, size_t buflen); + +// Generic messages +bool Android_JNI_SendMessage(int command, int param); + +// MessageBox +bool Android_JNI_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID); + +// Cursor support +int Android_JNI_CreateCustomCursor(SDL_Surface *surface, int hot_x, int hot_y); +void Android_JNI_DestroyCustomCursor(int cursorID); +bool Android_JNI_SetCustomCursor(int cursorID); +bool Android_JNI_SetSystemCursor(int cursorID); + +// Relative mouse support +bool Android_JNI_SupportsRelativeMouse(void); +bool Android_JNI_SetRelativeMouseEnabled(bool enabled); + +// Show toast notification +bool Android_JNI_ShowToast(const char *message, int duration, int gravity, int xOffset, int yOffset); + +bool Android_JNI_OpenURL(const char *url); + +int SDL_GetAndroidSDKVersion(void); + +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); + +// Ends C function definitions when using C++ +#ifdef __cplusplus +/* *INDENT-OFF* */ +} +/* *INDENT-ON* */ +#endif + +#endif // SDL_android_h -- cgit v1.2.3