From 712b456bbbd70886f933fb9e289686ff582dd991 Mon Sep 17 00:00:00 2001 From: Holden Ramsey Date: Sat, 27 Jun 2026 13:53:46 -0400 Subject: [PATCH] Android: decouple JNI setup from unused subsystems Query the natively compiled-in subsystems at runtime so the Java side only registers and initializes the managers that exist, fixing UnsatisfiedLinkError when SDL is built with a subsystem disabled (e.g. -DSDL_AUDIO_DISABLED). - SDL.setupJNI() calls nativeGetCompiledSubsystems() and skips SDLAudioManager/SDLControllerManager setup for absent subsystems. - Gate SDLControllerManager.initializeDeviceListener() and the joystick key-event path on the controller subsystem; gate the clipboard handler on video, mirroring the native guards. - Guard the SDLControllerManager JNI bindings with SDL_ANDROID_NEED_CONTROLLER_MANAGER (joystick or haptic enabled). - Reorganize SDL_android.h/.c into guarded per-subsystem groups with matching ordering. --- .../app/src/main/java/org/libsdl/app/SDL.java | 27 +- .../main/java/org/libsdl/app/SDLActivity.java | 63 +- .../org/libsdl/app/SDLControllerManager.java | 14 +- src/core/android/SDL_android.c | 4037 +++++++++-------- src/core/android/SDL_android.h | 91 +- src/joystick/android/SDL_sysjoystick.c | 4 + 6 files changed, 2177 insertions(+), 2059 deletions(-) diff --git a/android-project/app/src/main/java/org/libsdl/app/SDL.java b/android-project/app/src/main/java/org/libsdl/app/SDL.java index 097eb8cc873cc..9200e4ecd5cd0 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDL.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDL.java @@ -22,23 +22,28 @@ public class SDL { public static final int SDL_INIT_EVERYTHING = SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMEPAD | SDL_INIT_SENSOR | SDL_INIT_CAMERA; + // SDLControllerManager backs all three of these, so it is set up when any are present. + private static final int SDL_INIT_CONTROLLER = SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC; + private static int mInitializedSubsystems = SDL_INIT_EVERYTHING; static public void setupJNI() { setupJNI(SDL_INIT_EVERYTHING); } - // Mask must match the native build's SDL_*_DISABLED flags: dropping SDL_INIT_AUDIO when the lib was built with audio stalls checkJNIReady() and SDL_SetMainReady() never fires. static public void setupJNI(int subsystems) { - mInitializedSubsystems = subsystems; - SDLActivity.nativeSetupJNI(); - if ((subsystems & SDL_INIT_AUDIO) != 0) { + int compiled = SDLActivity.nativeGetCompiledSubsystems(); + mInitializedSubsystems = subsystems & compiled; + + if ((compiled & SDL_INIT_AUDIO) != 0) { SDLAudioManager.nativeSetupJNI(); } - SDLControllerManager.nativeSetupJNI(); + if ((compiled & SDL_INIT_CONTROLLER) != 0) { + SDLControllerManager.nativeSetupJNI(); + } } static public void initialize() { @@ -54,7 +59,17 @@ static public void initialize(int subsystems) { SDLAudioManager.initialize(); } - SDLControllerManager.initialize(); + if ((subsystems & SDL_INIT_CONTROLLER) != 0) { + SDLControllerManager.initialize(); + } + } + + static boolean isSubsystemInitialized(int subsystem) { + return (mInitializedSubsystems & subsystem) != 0; + } + + static boolean isControllerManagerReady() { + return isSubsystemInitialized(SDL_INIT_CONTROLLER); } // This function stores the current activity (SDL or not) diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java index 00df5f0864add..42549b4a76c90 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -467,22 +467,28 @@ public void onClick(DialogInterface dialog,int id) { mSingleton = this; SDL.setContext(this); - SDLControllerManager.initializeDeviceListener(); + if (SDL.isControllerManagerReady()) { + SDLControllerManager.initializeDeviceListener(); + } - mClipboardHandler = new SDLClipboardHandler(); + if (SDL.isSubsystemInitialized(SDL.SDL_INIT_VIDEO)) { + mClipboardHandler = new SDLClipboardHandler(); + } mHIDDeviceManager = HIDDeviceManager.acquire(this); // Set up the surface - mSurface = createSDLSurface(this); + if (SDL.isSubsystemInitialized(SDL.SDL_INIT_VIDEO)) { + mSurface = createSDLSurface(this); - mLayout = new RelativeLayout(this); - mLayout.addView(mSurface); + mLayout = new RelativeLayout(this); + mLayout.addView(mSurface); - // Get our current screen orientation and pass it down. - SDLActivity.nativeSetNaturalOrientation(SDLActivity.getNaturalOrientation()); - mCurrentRotation = SDLActivity.getCurrentRotation(); - SDLActivity.onNativeRotationChanged(mCurrentRotation); + // Get our current screen orientation and pass it down. + SDLActivity.nativeSetNaturalOrientation(SDLActivity.getNaturalOrientation()); + mCurrentRotation = SDLActivity.getCurrentRotation(); + SDLActivity.onNativeRotationChanged(mCurrentRotation); + } try { if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { @@ -502,19 +508,22 @@ public void onClick(DialogInterface dialog,int id) { break; } - setContentView(mLayout); - - setWindowStyle(false); + if (mLayout != null) { + setContentView(mLayout); + setWindowStyle(false); + } getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); // Get filename from "Open with" of another application - Intent intent = getIntent(); - if (intent != null && intent.getData() != null) { - String filename = intent.getData().getPath(); - if (filename != null) { - Log.v(TAG, "Got filename: " + filename); - SDLActivity.onNativeDropFile(filename); + if (SDL.isSubsystemInitialized(SDL.SDL_INIT_VIDEO)) { + Intent intent = getIntent(); + if (intent != null && intent.getData() != null) { + String filename = intent.getData().getPath(); + if (filename != null) { + Log.v(TAG, "Got filename: " + filename); + SDLActivity.onNativeDropFile(filename); + } } } } @@ -886,21 +895,27 @@ public static void handleNativeState() { // Try a transition to resumed state if (mNextNativeState == NativeState.RESUMED) { - if (mSurface.mIsSurfaceReady && (mHasFocus || mHasMultiWindow) && mIsResumedCalled) { + boolean readyToRun = (mSurface == null) ? mIsResumedCalled + : (mSurface.mIsSurfaceReady && (mHasFocus || mHasMultiWindow) && mIsResumedCalled); + if (readyToRun) { if (mSDLThread == null) { // This is the entry point to the C app. // Start up the C app thread and enable sensor input for the first time // FIXME: Why aren't we enabling sensor input at start? mSDLThread = new Thread(new SDLMain(), "SDLThread"); - mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); + if (mSurface != null) { + mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); + } mSDLThread.start(); // No nativeResume(), don't signal Android_ResumeSem } else { nativeResume(); } - mSurface.handleResume(); + if (mSurface != null) { + mSurface.handleResume(); + } mCurrentNativeState = mNextNativeState; } @@ -1058,7 +1073,8 @@ protected boolean sendCommand(int command, Object data) { DisplayMetrics realMetrics = new DisplayMetrics(); display.getRealMetrics(realMetrics); - boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && + boolean bFullscreenLayout = (mSurface != null) && + ((realMetrics.widthPixels == mSurface.getWidth()) && (realMetrics.heightPixels == mSurface.getHeight())); if ((Integer) data == 1) { @@ -1102,6 +1118,7 @@ protected boolean sendCommand(int command, Object data) { // C functions we call public static native String nativeGetVersion(); public static native void nativeSetupJNI(); + public static native int nativeGetCompiledSubsystems(); public static native void nativeInitMainThread(); public static native void nativeCleanupMainThread(); public static native int nativeRunMain(String library, String function, Object arguments); @@ -1553,7 +1570,7 @@ public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputC // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and // SOURCE_JOYSTICK, while its key events arrive from the keyboard source // So, retrieve the device itself and check all of its sources - if (SDLControllerManager.isDeviceSDLJoystick(device)) { + if (SDL.isControllerManagerReady() && SDLControllerManager.isDeviceSDLJoystick(device)) { // Note that we process events with specific key codes here if (event.getAction() == KeyEvent.ACTION_DOWN) { if (SDLControllerManager.onNativePadDown(deviceId, keyCode, event.getScanCode())) { diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java index 0e6c20494584b..5da5fe8193fe9 100644 --- a/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java +++ b/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java @@ -60,7 +60,7 @@ static void initialize() { mJoystickHandler = new SDLJoystickHandler(); } - if (mHapticHandler == null) { + if (mHapticHandler == null && SDL.isSubsystemInitialized(SDL.SDL_INIT_HAPTIC)) { if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { mHapticHandler = new SDLHapticHandler_API31(); } else if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { @@ -977,10 +977,12 @@ boolean setRelativeMouseEnabled(boolean enabled) { } if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) { - if (enabled) { - SDLActivity.getContentView().requestPointerCapture(); - } else { - SDLActivity.getContentView().releasePointerCapture(); + if (SDLActivity.getContentView() != null) { + if (enabled) { + SDLActivity.getContentView().requestPointerCapture(); + } else { + SDLActivity.getContentView().releasePointerCapture(); + } } mRelativeModeEnabled = enabled; return true; @@ -997,7 +999,7 @@ void reclaimRelativeMouseModeIfNeeded() { return; } - if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) { + if (mRelativeModeEnabled && !SDLActivity.isDeXMode() && SDLActivity.getContentView() != null) { SDLActivity.getContentView().requestPointerCapture(); } } diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c index fd95400d15b74..de8f1d39dfb2c 100644 --- a/src/core/android/SDL_android.c +++ b/src/core/android/SDL_android.c @@ -61,6 +61,110 @@ #define ENCODING_PCM_16BIT 2 #define ENCODING_PCM_FLOAT 4 +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; + +static jmethodID midGetContext; +static jmethodID midGetDeviceFormFactor; +static jmethodID midGetManifestEnvironmentVariables; +static jmethodID midIsAndroidTV; +static jmethodID midIsChromebook; +static jmethodID midIsDeXMode; +static jmethodID midIsTablet; +static jmethodID midOpenURL; +static jmethodID midRequestPermission; +static jmethodID midShowToast; +static jmethodID midSendMessage; +static jmethodID midOpenFileDescriptor; +static jmethodID midManualBackButton; +#ifndef SDL_DIALOG_DISABLED +static jmethodID midShowFileDialog; +#endif // !SDL_DIALOG_DISABLED +static jmethodID midGetPreferredLocales; + +#ifndef SDL_VIDEO_DISABLED +// Video/surface method signatures +static jmethodID midClipboardGetText; +static jmethodID midClipboardHasText; +static jmethodID midClipboardSetText; +static jmethodID midCreateCustomCursor; +static jmethodID midDestroyCustomCursor; +static jmethodID midGetNativeSurface; +static jmethodID midInitTouch; +static jmethodID midMinimizeWindow; +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; +#endif // !SDL_VIDEO_DISABLED + +#ifndef SDL_AUDIO_DISABLED +// audio manager +static jclass mAudioManagerClass; + +// method signatures +static jmethodID midRegisterAudioDeviceCallback; +static jmethodID midUnregisterAudioDeviceCallback; +static jmethodID midAudioSetThreadPriority; +#endif // !SDL_AUDIO_DISABLED + +// controller manager +#ifndef SDL_JOYSTICK_DISABLED +static jclass mControllerManagerClass; + +// method signatures +static jmethodID midDetectDevices; +static jmethodID midJoystickSetLED; +static jmethodID midJoystickSetSensorsEnabled; +#endif // !SDL_JOYSTICK_DISABLED + +#ifndef SDL_HAPTIC_DISABLED +static jmethodID midDetectHapticDevices; +static jmethodID midHapticRun; +static jmethodID midHapticRumble; +static jmethodID midHapticStop; +#endif // !SDL_HAPTIC_DISABLED + +#ifndef SDL_VIDEO_DISABLED +// display orientation +static SDL_DisplayOrientation displayNaturalOrientation; +static SDL_DisplayOrientation displayCurrentOrientation; +#endif // !SDL_VIDEO_DISABLED + +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 int Android_ActivityMutexCount = 0; +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; + // Java class SDLActivity JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetVersion)( JNIEnv *env, jclass cls); @@ -68,6 +172,9 @@ JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetVersion)( JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)( JNIEnv *env, jclass cls); +JNIEXPORT jint JNICALL SDL_JAVA_INTERFACE(nativeGetCompiledSubsystems)( + JNIEnv *env, jclass jcls); + JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeInitMainThread)( JNIEnv *env, jclass cls); @@ -147,45 +254,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePen)( JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeClipboardChanged)( JNIEnv *env, jclass jcls); -#endif // !SDL_VIDEO_DISABLED - -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); -#ifndef SDL_VIDEO_DISABLED JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetNaturalOrientation)( JNIEnv *env, jclass cls, jint orientation); @@ -201,72 +270,7 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeInsetsChanged)( JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeAddTouch)( JNIEnv *env, jclass cls, jint touchId, jstring name); -#endif // !SDL_VIDEO_DISABLED - -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", "()V", 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) }, - { "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) }, - { "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) }, -#ifndef SDL_VIDEO_DISABLED - // Video/input methods, registered only when the video subsystem is enabled - { "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) }, - { "onNativeScreenKeyboardShown", "()V", SDL_JAVA_INTERFACE(onNativeScreenKeyboardShown) }, - { "onNativeScreenKeyboardHidden", "()V", SDL_JAVA_INTERFACE(onNativeScreenKeyboardHidden) }, - { "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) }, - { "onNativePinchStart", "(FFFF)V", SDL_JAVA_INTERFACE(onNativePinchStart) }, - { "onNativePinchUpdate", "(FFFFF)V", SDL_JAVA_INTERFACE(onNativePinchUpdate) }, - { "onNativePinchEnd", "(FFFF)V", SDL_JAVA_INTERFACE(onNativePinchEnd) }, - { "onNativeMouse", "(IIFFZ)V", SDL_JAVA_INTERFACE(onNativeMouse) }, - { "onNativePen", "(IIIIFFF)V", SDL_JAVA_INTERFACE(onNativePen) }, - { "onNativeClipboardChanged", "()V", SDL_JAVA_INTERFACE(onNativeClipboardChanged) }, - { "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) } -#endif // !SDL_VIDEO_DISABLED -}; -#ifndef SDL_VIDEO_DISABLED // Java class SDLInputConnection JNIEXPORT void JNICALL SDL_JAVA_INTERFACE_INPUT_CONNECTION(nativeCommitText)( JNIEnv *env, jclass cls, @@ -280,1266 +284,1170 @@ 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) } }; -#endif // !SDL_VIDEO_DISABLED - -#ifndef SDL_AUDIO_DISABLED -// Java class SDLAudioManager -JNIEXPORT void JNICALL SDL_JAVA_AUDIO_INTERFACE(nativeSetupJNI)( - JNIEnv *env, jclass jcls); - -JNIEXPORT void JNICALL - SDL_JAVA_AUDIO_INTERFACE(nativeAddAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, jstring name, - jint device_id); - -JNIEXPORT void JNICALL - SDL_JAVA_AUDIO_INTERFACE(nativeRemoveAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, - jint device_id); - -static JNINativeMethod SDLAudioManager_tab[] = { - { "nativeSetupJNI", "()V", SDL_JAVA_AUDIO_INTERFACE(nativeSetupJNI) }, - { "nativeAddAudioDevice", "(ZLjava/lang/String;I)V", SDL_JAVA_AUDIO_INTERFACE(nativeAddAudioDevice) }, - { "nativeRemoveAudioDevice", "(ZI)V", SDL_JAVA_AUDIO_INTERFACE(nativeRemoveAudioDevice) } -}; -#endif // !SDL_AUDIO_DISABLED -// Java class SDLControllerManager -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI)( - JNIEnv *env, jclass jcls); - -JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( +// Drop file +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDropFile)( JNIEnv *env, jclass jcls, - jint device_id, jint keycode, jint scancode); + jstring filename) +{ + const char *path = (*env)->GetStringUTFChars(env, filename, NULL); + SDL_SendDropFile(NULL, NULL, path); + (*env)->ReleaseStringUTFChars(env, filename, path); + SDL_SendDropComplete(NULL); +} -JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp)( +// Set screen resolution +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetScreenResolution)( JNIEnv *env, jclass jcls, - jint device_id, jint keycode, jint scancode); + jint surfaceWidth, jint surfaceHeight, + jint deviceWidth, jint deviceHeight, jfloat density, jfloat rate) +{ + SDL_LockMutex(Android_ActivityMutex); -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy)( - JNIEnv *env, jclass jcls, - jint device_id, jint axis, jfloat value); + Android_SetScreenResolution(surfaceWidth, surfaceHeight, deviceWidth, deviceHeight, density, rate); -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat)( - JNIEnv *env, jclass jcls, - jint device_id, jint hat_id, jint x, jint y); + SDL_UnlockMutex(Android_ActivityMutex); +} -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoySensor)( - JNIEnv *env, jclass jcls, - jint device_id, jint sensor_type, jlong sensor_timestamp, jfloat x, jfloat y, jfloat z); +// Resize +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeResize)( + JNIEnv *env, jclass jcls) +{ + SDL_LockMutex(Android_ActivityMutex); -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, jboolean has_rgb_led, jboolean has_accelerometer, jboolean has_gyroscope); + if (Android_Window) { + Android_SendResize(Android_Window); + } -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick)( - JNIEnv *env, jclass jcls, - jint device_id); + SDL_UnlockMutex(Android_ActivityMutex); +} -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddHaptic)( +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetNaturalOrientation)( JNIEnv *env, jclass jcls, - jint device_id, jstring device_name); + jint orientation) +{ + displayNaturalOrientation = (SDL_DisplayOrientation)orientation; +} -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic)( +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeRotationChanged)( JNIEnv *env, jclass jcls, - jint device_id); - -static JNINativeMethod SDLControllerManager_tab[] = { - { "nativeSetupJNI", "()V", SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI) }, - { "onNativePadDown", "(III)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown) }, - { "onNativePadUp", "(III)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp) }, - { "onNativeJoy", "(IIF)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy) }, - { "onNativeHat", "(IIII)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat) }, - { "onNativeJoySensor", "(IIJFFF)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoySensor) }, - { "nativeAddJoystick", "(ILjava/lang/String;Ljava/lang/String;IIIIIIZZZZ)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) } -}; + jint rotation) +{ + SDL_LockMutex(Android_ActivityMutex); -// Uncomment this to log messages entering and exiting methods in this file -// #define DEBUG_JNI + if (displayNaturalOrientation == SDL_ORIENTATION_LANDSCAPE) { + rotation += 90; + } -static void checkJNIReady(void); + 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; + } -/******************************************************************************* - This file links the Java side of Android with libsdl -*******************************************************************************/ -#include + Android_SetOrientation(displayCurrentOrientation); -/******************************************************************************* - Globals -*******************************************************************************/ -static pthread_key_t mThreadKey; -static pthread_once_t key_once = PTHREAD_ONCE_INIT; -static JavaVM *mJavaVM = NULL; + SDL_UnlockMutex(Android_ActivityMutex); +} -// Main activity -static jclass mActivityClass; +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeInsetsChanged)( + JNIEnv *env, jclass jcls, + jint left, jint right, jint top, jint bottom) +{ + SDL_LockMutex(Android_ActivityMutex); -static jmethodID midGetContext; -static jmethodID midGetDeviceFormFactor; -static jmethodID midGetManifestEnvironmentVariables; -static jmethodID midIsAndroidTV; -static jmethodID midIsChromebook; -static jmethodID midIsDeXMode; -static jmethodID midIsTablet; -static jmethodID midOpenURL; -static jmethodID midRequestPermission; -static jmethodID midShowToast; -static jmethodID midSendMessage; -static jmethodID midOpenFileDescriptor; -static jmethodID midShowFileDialog; -static jmethodID midGetPreferredLocales; + Android_SetWindowSafeAreaInsets(left, right, top, bottom); -#ifndef SDL_VIDEO_DISABLED -// Video/surface method signatures -static jmethodID midClipboardGetText; -static jmethodID midClipboardHasText; -static jmethodID midClipboardSetText; -static jmethodID midCreateCustomCursor; -static jmethodID midDestroyCustomCursor; -static jmethodID midGetNativeSurface; -static jmethodID midInitTouch; -static jmethodID midManualBackButton; -static jmethodID midMinimizeWindow; -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; -#endif // !SDL_VIDEO_DISABLED + SDL_UnlockMutex(Android_ActivityMutex); +} -#ifndef SDL_AUDIO_DISABLED -// audio manager -static jclass mAudioManagerClass; +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeAddTouch)( + JNIEnv *env, jclass cls, + jint touchId, jstring name) +{ + const char *utfname = (*env)->GetStringUTFChars(env, name, NULL); -// method signatures -static jmethodID midRegisterAudioDeviceCallback; -static jmethodID midUnregisterAudioDeviceCallback; -static jmethodID midAudioSetThreadPriority; -#endif // !SDL_AUDIO_DISABLED + SDL_AddTouch(Android_ConvertJavaTouchID(touchId), + SDL_TOUCH_DEVICE_DIRECT, utfname); -// controller manager -static jclass mControllerManagerClass; + (*env)->ReleaseStringUTFChars(env, name, utfname); +} -// method signatures -static jmethodID midDetectDevices; -static jmethodID midJoystickSetLED; -static jmethodID midJoystickSetSensorsEnabled; -static jmethodID midDetectHapticDevices; -static jmethodID midHapticRun; -static jmethodID midHapticRumble; -static jmethodID midHapticStop; +// Called from surfaceCreated() +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceCreated)(JNIEnv *env, jclass jcls) +{ + SDL_LockMutex(Android_ActivityMutex); -// display orientation -static SDL_DisplayOrientation displayNaturalOrientation; -static SDL_DisplayOrientation displayCurrentOrientation; + if (Android_Window) { + SDL_WindowData *data = Android_Window->internal; -static bool bHasEnvironmentVariables; + 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"); + } + } -// 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; + SDL_UnlockMutex(Android_ActivityMutex); +} -static SDL_Mutex *Android_ActivityMutex = NULL; -static int Android_ActivityMutexCount = 0; -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; +// Called from surfaceChanged() +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceChanged)(JNIEnv *env, jclass jcls) +{ + SDL_LockMutex(Android_ActivityMutex); -/******************************************************************************* - Functions called by JNI -*******************************************************************************/ +#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; -/* 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 - */ + // 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); + } -/* 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) - */ + // GL Context handling is done in the event loop because this function is run from the Java thread + } +#endif -// 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; + if (Android_Window) { + Android_RestoreScreenKeyboard(SDL_GetVideoDevice(), Android_Window); } - return true; + + SDL_UnlockMutex(Android_ActivityMutex); } -// Get local storage value -JNIEnv *Android_JNI_GetEnv(void) +// Called from surfaceDestroyed() +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceDestroyed)(JNIEnv *env, jclass jcls) { - // 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; + int nb_attempt = 50; - // There should be a JVM - if (!mJavaVM) { - __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed, there is no JavaVM"); - return NULL; - } +retry: - /* 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; + 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; + } } - // Save JNIEnv into the Thread local storage - if (!Android_JNI_SetEnv(env)) { - return NULL; +#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 } - return env; + SDL_UnlockMutex(Android_ActivityMutex); } -// Set up an external thread for using JNI with Android_JNI_GetEnv() -bool Android_JNI_SetupThread(void) +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeScreenKeyboardShown)(JNIEnv *env, jclass jcls) { - JNIEnv *env; - int status; + SDL_SendScreenKeyboardShown(); +} - // There should be a JVM - if (!mJavaVM) { - __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed, there is no JavaVM"); - return false; - } +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeScreenKeyboardHidden)(JNIEnv *env, jclass jcls) +{ + SDL_SendScreenKeyboardHidden(); +} - /* 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; - } +// Keydown +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyDown)( + JNIEnv *env, jclass jcls, + jint keycode) +{ + SDL_LockMutex(Android_ActivityMutex); - // Save JNIEnv into the Thread local storage - if (!Android_JNI_SetEnv(env)) { - return false; + if (Android_Window) { + Android_OnKeyDown(keycode); } - return true; + SDL_UnlockMutex(Android_ActivityMutex); } -// Destructor called for each thread where mThreadKey is not NULL -static void Android_JNI_ThreadDestroyed(void *value) +// Keyup +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyUp)( + JNIEnv *env, jclass jcls, + jint keycode) { - // 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); + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + Android_OnKeyUp(keycode); } + + SDL_UnlockMutex(Android_ActivityMutex); } -// Creation of local storage mThreadKey -static void Android_JNI_CreateKey(void) +// Virtual keyboard return key might stop text input +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(onNativeSoftReturnKey)( + JNIEnv *env, jclass jcls) { - 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); + if (SDL_GetHintBoolean(SDL_HINT_RETURN_KEY_HIDES_IME, false)) { + SDL_StopTextInput(Android_Window); + return JNI_TRUE; } + return JNI_FALSE; } -static void Android_JNI_CreateKey_once(void) +// Keyboard Focus Lost +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyboardFocusLost)( + JNIEnv *env, jclass jcls) { - 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); - } + // Calling SDL_StopTextInput will take care of hiding the keyboard and cleaning up the DummyText widget + SDL_StopTextInput(Android_Window); } -static void register_methods(JNIEnv *env, const char *classname, JNINativeMethod *methods, int nb) +// 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) { - 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; - } + SDL_LockMutex(Android_ActivityMutex); + + Android_OnTouch(Android_Window, touch_device_id_in, pointer_finger_id_in, action, x, y, p); + + SDL_UnlockMutex(Android_ActivityMutex); } -// Library init -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) +// Pinch +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePinchStart)( + JNIEnv *env, jclass jcls, jfloat span_x, jfloat span_y, jfloat focus_x, jfloat focus_y) { - JNIEnv *env = NULL; - - mJavaVM = vm; + SDL_LockMutex(Android_ActivityMutex); - 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; + if (Android_Window) { + SDL_SendPinch(SDL_EVENT_PINCH_BEGIN, 0, Android_Window, 0, span_x, span_y, focus_x, focus_y); } - register_methods(env, "org/libsdl/app/SDLActivity", SDLActivity_tab, SDL_arraysize(SDLActivity_tab)); -#ifndef SDL_VIDEO_DISABLED - register_methods(env, "org/libsdl/app/SDLInputConnection", SDLInputConnection_tab, SDL_arraysize(SDLInputConnection_tab)); -#endif -#ifndef SDL_AUDIO_DISABLED - register_methods(env, "org/libsdl/app/SDLAudioManager", SDLAudioManager_tab, SDL_arraysize(SDLAudioManager_tab)); -#endif - 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; + SDL_UnlockMutex(Android_ActivityMutex); } -void checkJNIReady(void) +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePinchUpdate)( + JNIEnv *env, jclass jcls, jfloat scale, jfloat span_x, jfloat span_y, jfloat focus_x, jfloat focus_y) { - if (!mActivityClass || !mControllerManagerClass) { - // We aren't fully initialized, let's just return. - return; + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + SDL_SendPinch(SDL_EVENT_PINCH_UPDATE, 0, Android_Window, scale, span_x, span_y, focus_x, focus_y); } -#ifndef SDL_AUDIO_DISABLED - if (!mAudioManagerClass) { - return; + SDL_UnlockMutex(Android_ActivityMutex); +} + +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePinchEnd)( + JNIEnv *env, jclass jcls, jfloat span_x, jfloat span_y, jfloat focus_x, jfloat focus_y) +{ + SDL_LockMutex(Android_ActivityMutex); + + if (Android_Window) { + SDL_SendPinch(SDL_EVENT_PINCH_END, 0, Android_Window, 0, span_x, span_y, focus_x, focus_y); } -#endif - SDL_SetMainReady(); + SDL_UnlockMutex(Android_ActivityMutex); } -// Get SDL version -- called before SDL_main() to verify JNI bindings -JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetVersion)(JNIEnv *env, jclass cls) +// Mouse +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeMouse)( + JNIEnv *env, jclass jcls, + jint button, jint action, jfloat x, jfloat y, jboolean relative) { - char version[128]; + SDL_LockMutex(Android_ActivityMutex); - SDL_snprintf(version, sizeof(version), "%d.%d.%d", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_MICRO_VERSION); + Android_OnMouse(Android_Window, button, action, x, y, relative); - return (*env)->NewStringUTF(env, version); + SDL_UnlockMutex(Android_ActivityMutex); } -// Activity initialization -- called before SDL_main() to initialize JNI bindings -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cls) +// Pen +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePen)( + JNIEnv *env, jclass jcls, + jint pen_id_in, jint device_type, jint button, jint action, jfloat x, jfloat y, jfloat p) { - __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeSetupJNI()"); + SDL_LockMutex(Android_ActivityMutex); - // Start with a clean slate - SDL_ClearError(); + Android_OnPen(Android_Window, pen_id_in, device_type, button, action, x, y, p); - /* - * 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(); + SDL_UnlockMutex(Android_ActivityMutex); +} - // Save JNIEnv of SDLActivity - Android_JNI_SetEnv(env); +// Clipboard +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeClipboardChanged)( + JNIEnv *env, jclass jcls) +{ + // TODO: compute new mime types + SDL_SendClipboardUpdate(false, NULL, 0); +} +#endif // !SDL_VIDEO_DISABLED - if (!mJavaVM) { - __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to found a JavaVM"); - } +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeLowMemory)( + JNIEnv *env, jclass cls); - /* 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 ? - } +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeLocaleChanged)( + JNIEnv *env, jclass cls); - if (!Android_ActivityMutex) { - __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_ActivityMutex mutex"); - } +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDarkModeChanged)( + JNIEnv *env, jclass cls, jboolean enabled); - Android_LifecycleMutex = SDL_CreateMutex(); - if (!Android_LifecycleMutex) { - __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_LifecycleMutex mutex"); - } +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSendQuit)( + JNIEnv *env, jclass cls); - Android_LifecycleEventSem = SDL_CreateSemaphore(0); - if (!Android_LifecycleEventSem) { - __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to create Android_LifecycleEventSem semaphore"); - } +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeQuit)( + JNIEnv *env, jclass cls); - mActivityClass = (jclass)((*env)->NewGlobalRef(env, cls)); +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePause)( + JNIEnv *env, jclass cls); - midGetContext = (*env)->GetStaticMethodID(env, mActivityClass, "getContext", "()Landroid/app/Activity;"); - midGetDeviceFormFactor = (*env)->GetStaticMethodID(env, mActivityClass, "getDeviceFormFactor", "()Ljava/lang/String;"); - midGetManifestEnvironmentVariables = (*env)->GetStaticMethodID(env, mActivityClass, "getManifestEnvironmentVariables", "()Z"); - midIsAndroidTV = (*env)->GetStaticMethodID(env, mActivityClass, "isAndroidTV", "()Z"); - midIsChromebook = (*env)->GetStaticMethodID(env, mActivityClass, "isChromebook", "()Z"); - midIsDeXMode = (*env)->GetStaticMethodID(env, mActivityClass, "isDeXMode", "()Z"); - midIsTablet = (*env)->GetStaticMethodID(env, mActivityClass, "isTablet", "()Z"); - 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"); - midOpenFileDescriptor = (*env)->GetStaticMethodID(env, mActivityClass, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I"); - midShowFileDialog = (*env)->GetStaticMethodID(env, mActivityClass, "showFileDialog", "([Ljava/lang/String;ZILjava/lang/String;I)Z"); - midGetPreferredLocales = (*env)->GetStaticMethodID(env, mActivityClass, "getPreferredLocales", "()Ljava/lang/String;"); - - if (!midGetContext || - !midGetDeviceFormFactor || - !midGetManifestEnvironmentVariables || - !midIsAndroidTV || - !midIsChromebook || - !midIsDeXMode || - !midIsTablet || - !midOpenURL || - !midRequestPermission || - !midShowToast || - !midSendMessage || - !midOpenFileDescriptor || - !midShowFileDialog || - !midGetPreferredLocales) { - __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some core Java callbacks, do you have the latest version of SDLActivity.java?"); - } - -#ifndef SDL_VIDEO_DISABLED - 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"); - midGetNativeSurface = (*env)->GetStaticMethodID(env, mActivityClass, "getNativeSurface", "()Landroid/view/Surface;"); - midInitTouch = (*env)->GetStaticMethodID(env, mActivityClass, "initTouch", "()V"); - midManualBackButton = (*env)->GetStaticMethodID(env, mActivityClass, "manualBackButton", "()V"); - midMinimizeWindow = (*env)->GetStaticMethodID(env, mActivityClass, "minimizeWindow", "()V"); - 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"); - - if (!midClipboardGetText || - !midClipboardHasText || - !midClipboardSetText || - !midCreateCustomCursor || - !midDestroyCustomCursor || - !midGetNativeSurface || - !midInitTouch || - !midManualBackButton || - !midMinimizeWindow || - !midSetActivityTitle || - !midSetCustomCursor || - !midSetOrientation || - !midSetRelativeMouseEnabled || - !midSetSystemCursor || - !midSetWindowStyle || - !midShouldMinimizeOnFocusLoss || - !midShowTextInput || - !midSupportsRelativeMouse) { - __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some video Java callbacks, do you have the latest version of SDLActivity.java?"); - } -#endif // !SDL_VIDEO_DISABLED - - checkJNIReady(); -} - -#ifndef SDL_AUDIO_DISABLED -// 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(); -} -#endif // !SDL_AUDIO_DISABLED - -// 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)); - - midDetectDevices = (*env)->GetStaticMethodID(env, mControllerManagerClass, - "detectDevices", "()V"); - midJoystickSetLED = (*env)->GetStaticMethodID(env, mControllerManagerClass, - "joystickSetLED", "(IIII)V"); - midJoystickSetSensorsEnabled = (*env)->GetStaticMethodID(env, mControllerManagerClass, - "joystickSetSensorsEnabled", "(IZ)V"); - midDetectHapticDevices = (*env)->GetStaticMethodID(env, mControllerManagerClass, - "detectHapticDevices", "()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"); +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeResume)( + JNIEnv *env, jclass cls); - if (!midDetectDevices || !midJoystickSetLED || !midJoystickSetSensorsEnabled || !midDetectHapticDevices || !midHapticRun || !midHapticRumble || !midHapticStop) { - __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some Java callbacks, do you have the latest version of SDLControllerManager.java?"); - } +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeFocusChanged)( + JNIEnv *env, jclass cls, jboolean hasFocus); - checkJNIReady(); -} +JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetHint)( + JNIEnv *env, jclass cls, + jstring name); -static int run_count = 0; -static bool allow_recreate_activity; -static bool allow_recreate_activity_set; +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeGetHintBoolean)( + JNIEnv *env, jclass cls, + jstring name, jboolean default_value); -JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeCheckSDLThreadCounter)( - JNIEnv *env, jclass jcls) -{ - int tmp = run_count; - run_count += 1; - return tmp; -} +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetenv)( + JNIEnv *env, jclass cls, + jstring name, jstring value); -void Android_SetAllowRecreateActivity(bool enabled) -{ - allow_recreate_activity = enabled; - allow_recreate_activity_set = true; -} +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) -{ - return allow_recreate_activity; -} - -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) { - // Use the name "app_process" for argv[0] so PHYSFS_platformCalcBaseDir() works. - // https://github.com/love2d/love-android/issues/24 - // (note that PhysicsFS hasn't used argv on Android in a long time, but we'll keep this for compat at least for SDL3's lifetime. --ryan.) - const char *argv0 = "app_process"; - const int len = (*env)->GetArrayLength(env, array); // argv elements, not counting argv[0]. - - size_t total_alloc_len = (SDL_strlen(argv0) + 1) + ((len + 2) * sizeof (char *)); // len+2 to allocate an array that also holds argv0 and a NULL terminator. - for (int i = 0; i < len; ++i) { - total_alloc_len++; // null terminator. - jstring string = (*env)->GetObjectArrayElement(env, array, i); - if (string) { - const char *utf = (*env)->GetStringUTFChars(env, string, 0); - if (utf) { - total_alloc_len += SDL_strlen(utf) + 1; - (*env)->ReleaseStringUTFChars(env, string, utf); - } - (*env)->DeleteLocalRef(env, string); - } - } - - void *args = malloc(total_alloc_len); // This should NOT be SDL_malloc() - if (!args) { // uhoh. - __android_log_print(ANDROID_LOG_ERROR, "SDL", "nativeRunMain(): Out of memory parsing command line!"); - } else { - size_t remain = total_alloc_len - (sizeof (char *) * (len + 2)); - int argc = 0; - char **argv = (char **) args; - char *ptr = (char *) &argv[len + 2]; - size_t cpy = SDL_strlcpy(ptr, argv0, remain) + 1; - argv[argc++] = ptr; - SDL_assert(cpy <= remain); remain -= cpy; ptr += cpy; - for (int i = 0; i < len; ++i) { - jstring string = (*env)->GetObjectArrayElement(env, array, i); - const char *utf = string ? (*env)->GetStringUTFChars(env, string, 0) : NULL; - cpy = SDL_strlcpy(ptr, utf ? utf : "", remain) + 1; - if (cpy < remain) { - argv[argc++] = ptr; - remain -= cpy; - ptr += cpy; - } - if (utf) { - (*env)->ReleaseStringUTFChars(env, string, utf); - } - if (string) { - (*env)->DeleteLocalRef(env, string); - } - } - argv[argc] = NULL; - - // Run the application. - status = SDL_RunApp(argc, argv, SDL_main, NULL); - - // Release the arguments. - free(args); // This should NOT be SDL_free() - } - } 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); + JNIEnv *env, jclass jcls); - dlclose(library_handle); +JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeCheckSDLThreadCounter)( + JNIEnv *env, jclass jcls); - } else { - __android_log_print(ANDROID_LOG_ERROR, "SDL", "nativeRunMain(): Couldn't load library %s", library_file); - } - (*env)->ReleaseStringUTFChars(env, library, library_file); +#ifndef SDL_DIALOG_DISABLED +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeFileDialog)( + JNIEnv *env, jclass jcls, + jint requestCode, jobjectArray fileList, jint filter); +#endif // !SDL_DIALOG_DISABLED - // Do not issue an exit or the whole application will terminate instead of just the SDL thread - // exit(status); +static JNINativeMethod SDLActivity_tab[] = { + { "nativeGetVersion", "()Ljava/lang/String;", SDL_JAVA_INTERFACE(nativeGetVersion) }, + { "nativeSetupJNI", "()V", SDL_JAVA_INTERFACE(nativeSetupJNI) }, + { "nativeGetCompiledSubsystems", "()I", SDL_JAVA_INTERFACE(nativeGetCompiledSubsystems) }, + { "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) }, + { "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) }, + { "nativePermissionResult", "(IZ)V", SDL_JAVA_INTERFACE(nativePermissionResult) }, + { "nativeAllowRecreateActivity", "()Z", SDL_JAVA_INTERFACE(nativeAllowRecreateActivity) }, + { "nativeCheckSDLThreadCounter", "()I", SDL_JAVA_INTERFACE(nativeCheckSDLThreadCounter) }, +#ifndef SDL_DIALOG_DISABLED + { "onNativeFileDialog", "(I[Ljava/lang/String;I)V", SDL_JAVA_INTERFACE(onNativeFileDialog) }, +#endif // !SDL_DIALOG_DISABLED +#ifndef SDL_VIDEO_DISABLED + // Video/input methods, registered only when the video subsystem is enabled + { "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) }, + { "onNativeScreenKeyboardShown", "()V", SDL_JAVA_INTERFACE(onNativeScreenKeyboardShown) }, + { "onNativeScreenKeyboardHidden", "()V", SDL_JAVA_INTERFACE(onNativeScreenKeyboardHidden) }, + { "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) }, + { "onNativePinchStart", "(FFFF)V", SDL_JAVA_INTERFACE(onNativePinchStart) }, + { "onNativePinchUpdate", "(FFFFF)V", SDL_JAVA_INTERFACE(onNativePinchUpdate) }, + { "onNativePinchEnd", "(FFFF)V", SDL_JAVA_INTERFACE(onNativePinchEnd) }, + { "onNativeMouse", "(IIFFZ)V", SDL_JAVA_INTERFACE(onNativeMouse) }, + { "onNativePen", "(IIIIFFF)V", SDL_JAVA_INTERFACE(onNativePen) }, + { "onNativeClipboardChanged", "()V", SDL_JAVA_INTERFACE(onNativeClipboardChanged) }, + { "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) } +#endif // !SDL_VIDEO_DISABLED +}; - return status; -} +#ifndef SDL_AUDIO_DISABLED +// Java class SDLAudioManager +JNIEXPORT void JNICALL SDL_JAVA_AUDIO_INTERFACE(nativeSetupJNI)( + JNIEnv *env, jclass jcls); -static int FindLifecycleEvent(SDL_AndroidLifecycleEvent event) -{ - for (int index = 0; index < Android_NumLifecycleEvents; ++index) { - if (Android_LifecycleEvents[index] == event) { - return index; - } - } - return -1; -} +JNIEXPORT void JNICALL + SDL_JAVA_AUDIO_INTERFACE(nativeAddAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, jstring name, + jint device_id); -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; -} +JNIEXPORT void JNICALL + SDL_JAVA_AUDIO_INTERFACE(nativeRemoveAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, + jint device_id); -void Android_SendLifecycleEvent(SDL_AndroidLifecycleEvent event) -{ - SDL_LockMutex(Android_LifecycleMutex); - { - int index; - bool add_event = true; +static JNINativeMethod SDLAudioManager_tab[] = { + { "nativeSetupJNI", "()V", SDL_JAVA_AUDIO_INTERFACE(nativeSetupJNI) }, + { "nativeAddAudioDevice", "(ZLjava/lang/String;I)V", SDL_JAVA_AUDIO_INTERFACE(nativeAddAudioDevice) }, + { "nativeRemoveAudioDevice", "(ZI)V", SDL_JAVA_AUDIO_INTERFACE(nativeRemoveAudioDevice) } +}; +#endif // !SDL_AUDIO_DISABLED - 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; - } +// Java class SDLControllerManager +#ifndef SDL_JOYSTICK_DISABLED +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI)( + JNIEnv *env, jclass jcls); - if (add_event) { - SDL_assert(Android_NumLifecycleEvents < SDL_arraysize(Android_LifecycleEvents)); - Android_LifecycleEvents[Android_NumLifecycleEvents++] = event; - SDL_SignalSemaphore(Android_LifecycleEventSem); - } - } - SDL_UnlockMutex(Android_LifecycleMutex); -} +JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( + JNIEnv *env, jclass jcls, + jint device_id, jint keycode, jint scancode); -bool Android_WaitLifecycleEvent(SDL_AndroidLifecycleEvent *event, Sint64 timeoutNS) -{ - bool got_event = false; +JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp)( + JNIEnv *env, jclass jcls, + jint device_id, jint keycode, jint scancode); - int relock_count = 0; - Android_LockActivityMutex(); - while (Android_ActivityMutexCount > 1) { - // We came into this function with the activity lock held, we need to unlock so lifecycle events can be dispatched - ++relock_count; - Android_UnlockActivityMutex(); - } - Android_UnlockActivityMutex(); +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy)( + JNIEnv *env, jclass jcls, + jint device_id, jint axis, jfloat value); - 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); - } +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat)( + JNIEnv *env, jclass jcls, + jint device_id, jint hat_id, jint x, jint y); - while (relock_count > 0) { - Android_LockActivityMutex(); - --relock_count; - } - return got_event; -} +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoySensor)( + JNIEnv *env, jclass jcls, + jint device_id, jint sensor_type, jlong sensor_timestamp, jfloat x, jfloat y, jfloat z); -void Android_LockActivityMutex(void) -{ - SDL_LockMutex(Android_ActivityMutex); - ++Android_ActivityMutexCount; -} +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, jboolean has_rgb_led, jboolean has_accelerometer, jboolean has_gyroscope); -void Android_UnlockActivityMutex(void) -{ - --Android_ActivityMutexCount; - SDL_UnlockMutex(Android_ActivityMutex); -} +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick)( + JNIEnv *env, jclass jcls, + jint device_id); +#endif // !SDL_JOYSTICK_DISABLED -#ifndef SDL_VIDEO_DISABLED -// Drop file -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDropFile)( +#ifndef SDL_HAPTIC_DISABLED +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddHaptic)( 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); -} + jint device_id, jstring device_name); -// Set screen resolution -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetScreenResolution)( +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic)( JNIEnv *env, jclass jcls, - jint surfaceWidth, jint surfaceHeight, - jint deviceWidth, jint deviceHeight, jfloat density, jfloat rate) -{ - SDL_LockMutex(Android_ActivityMutex); + jint device_id); +#endif // !SDL_HAPTIC_DISABLED - Android_SetScreenResolution(surfaceWidth, surfaceHeight, deviceWidth, deviceHeight, density, rate); +// The SDLControllerManager Java class backs both the joystick and haptic subsystems. +// Haptic requires joystick (enforced in CMake), so gating on joystick covers both. +#if !defined(SDL_JOYSTICK_DISABLED) +#define SDL_ANDROID_NEED_CONTROLLER_MANAGER - SDL_UnlockMutex(Android_ActivityMutex); -} +static JNINativeMethod SDLControllerManager_tab[] = { + { "nativeSetupJNI", "()V", SDL_JAVA_CONTROLLER_INTERFACE(nativeSetupJNI) }, + { "onNativePadDown", "(III)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown) }, + { "onNativePadUp", "(III)Z", SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp) }, + { "onNativeJoy", "(IIF)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy) }, + { "onNativeHat", "(IIII)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat) }, + { "onNativeJoySensor", "(IIJFFF)V", SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoySensor) }, + { "nativeAddJoystick", "(ILjava/lang/String;Ljava/lang/String;IIIIIIZZZZ)V", SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick) }, + { "nativeRemoveJoystick", "(I)V", SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick) }, +#ifndef SDL_HAPTIC_DISABLED + { "nativeAddHaptic", "(ILjava/lang/String;)V", SDL_JAVA_CONTROLLER_INTERFACE(nativeAddHaptic) }, + { "nativeRemoveHaptic", "(I)V", SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic) } +#endif // !SDL_HAPTIC_DISABLED +}; +#endif // SDL_ANDROID_NEED_CONTROLLER_MANAGER -// Resize -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeResize)( - JNIEnv *env, jclass jcls) -{ - SDL_LockMutex(Android_ActivityMutex); +// Uncomment this to log messages entering and exiting methods in this file +// #define DEBUG_JNI - if (Android_Window) { - Android_SendResize(Android_Window); - } - SDL_UnlockMutex(Android_ActivityMutex); -} +/******************************************************************************* + Functions called by JNI +*******************************************************************************/ -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetNaturalOrientation)( - JNIEnv *env, jclass jcls, - jint orientation) +/* 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) { - displayNaturalOrientation = (SDL_DisplayOrientation)orientation; + 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; } -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeRotationChanged)( - JNIEnv *env, jclass jcls, - jint rotation) -{ - SDL_LockMutex(Android_ActivityMutex); - if (displayNaturalOrientation == SDL_ORIENTATION_LANDSCAPE) { - rotation += 90; +// 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); } +} - 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; +// 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); } +} - Android_SetOrientation(displayCurrentOrientation); +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); + } +} - SDL_UnlockMutex(Android_ActivityMutex); +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; + } } -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeInsetsChanged)( - JNIEnv *env, jclass jcls, - jint left, jint right, jint top, jint bottom) +// Library init +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { - SDL_LockMutex(Android_ActivityMutex); - - Android_SetWindowSafeAreaInsets(left, right, top, bottom); + JNIEnv *env = NULL; - SDL_UnlockMutex(Android_ActivityMutex); -} + mJavaVM = vm; -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeAddTouch)( - JNIEnv *env, jclass cls, - jint touchId, jstring name) -{ - const char *utfname = (*env)->GetStringUTFChars(env, name, NULL); + 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; + } - SDL_AddTouch(Android_ConvertJavaTouchID(touchId), - SDL_TOUCH_DEVICE_DIRECT, utfname); + register_methods(env, "org/libsdl/app/SDLActivity", SDLActivity_tab, SDL_arraysize(SDLActivity_tab)); +#ifndef SDL_VIDEO_DISABLED + register_methods(env, "org/libsdl/app/SDLInputConnection", SDLInputConnection_tab, SDL_arraysize(SDLInputConnection_tab)); +#endif +#ifndef SDL_AUDIO_DISABLED + register_methods(env, "org/libsdl/app/SDLAudioManager", SDLAudioManager_tab, SDL_arraysize(SDLAudioManager_tab)); +#endif +#ifdef SDL_ANDROID_NEED_CONTROLLER_MANAGER + register_methods(env, "org/libsdl/app/SDLControllerManager", SDLControllerManager_tab, SDL_arraysize(SDLControllerManager_tab)); +#endif + register_methods(env, "org/libsdl/app/HIDDeviceManager", HIDDeviceManager_tab, SDL_arraysize(HIDDeviceManager_tab)); - (*env)->ReleaseStringUTFChars(env, name, utfname); + return JNI_VERSION_1_4; } -#endif // !SDL_VIDEO_DISABLED -#ifndef SDL_AUDIO_DISABLED -JNIEXPORT void JNICALL -SDL_JAVA_AUDIO_INTERFACE(nativeAddAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, - jstring name, jint device_id) +void checkJNIReady(void) { -#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); - } + if (!mActivityClass) { + // We aren't fully initialized, let's just return. + return; + } + +#ifdef SDL_ANDROID_NEED_CONTROLLER_MANAGER + if (!mControllerManagerClass) { + return; } #endif -} -JNIEXPORT void JNICALL -SDL_JAVA_AUDIO_INTERFACE(nativeRemoveAudioDevice)(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))); +#ifndef SDL_AUDIO_DISABLED + if (!mAudioManagerClass) { + return; } #endif -} -#endif // !SDL_AUDIO_DISABLED -// Paddown -JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( - JNIEnv *env, jclass jcls, - jint device_id, jint keycode, jint scancode) -{ -#ifdef SDL_JOYSTICK_ANDROID - return Android_OnPadDown(device_id, keycode, scancode); -#else - return false; -#endif // SDL_JOYSTICK_ANDROID + SDL_SetMainReady(); } -// Padup -JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp)( - JNIEnv *env, jclass jcls, - jint device_id, jint keycode, jint scancode) +// Get SDL version -- called before SDL_main() to verify JNI bindings +JNIEXPORT jstring JNICALL SDL_JAVA_INTERFACE(nativeGetVersion)(JNIEnv *env, jclass cls) { -#ifdef SDL_JOYSTICK_ANDROID - return Android_OnPadUp(device_id, keycode, scancode); -#else - return false; -#endif // SDL_JOYSTICK_ANDROID -} + char version[128]; -// 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 -} + SDL_snprintf(version, sizeof(version), "%d.%d.%d", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_MICRO_VERSION); -// 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 + return (*env)->NewStringUTF(env, version); } -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoySensor)( - JNIEnv *env, jclass jcls, - jint device_id, jint sensor_type, jlong sensor_timestamp, jfloat x, jfloat y, jfloat z) +// Activity initialization -- called before SDL_main() to initialize JNI bindings +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cls) { -#ifdef SDL_JOYSTICK_ANDROID - // In Java there's no Uint64 type, so pass Sint64 as if it was Uint64. - Android_OnJoySensor(device_id, sensor_type, sensor_timestamp, x, y, z); -#endif // SDL_JOYSTICK_ANDROID -} + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeSetupJNI()"); -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, jboolean has_rgb_led, - jboolean has_accelerometer, jboolean has_gyroscope) -{ -#ifdef SDL_JOYSTICK_ANDROID - const char *name = (*env)->GetStringUTFChars(env, device_name, NULL); - const char *desc = (*env)->GetStringUTFChars(env, device_desc, NULL); + // Start with a clean slate + SDL_ClearError(); - Android_AddJoystick(device_id, name, desc, vendor_id, product_id, button_mask, naxes, axis_mask, nhats, - can_rumble, has_rgb_led, has_accelerometer, has_gyroscope); + /* + * 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(); - (*env)->ReleaseStringUTFChars(env, device_name, name); - (*env)->ReleaseStringUTFChars(env, device_desc, desc); -#endif // SDL_JOYSTICK_ANDROID -} + // Save JNIEnv of SDLActivity + Android_JNI_SetEnv(env); -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 -} + if (!mJavaVM) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "failed to found a JavaVM"); + } -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); + /* 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 ? + } - Android_AddHaptic(device_id, name); + 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)); + + midGetContext = (*env)->GetStaticMethodID(env, mActivityClass, "getContext", "()Landroid/app/Activity;"); + midGetDeviceFormFactor = (*env)->GetStaticMethodID(env, mActivityClass, "getDeviceFormFactor", "()Ljava/lang/String;"); + midGetManifestEnvironmentVariables = (*env)->GetStaticMethodID(env, mActivityClass, "getManifestEnvironmentVariables", "()Z"); + midIsAndroidTV = (*env)->GetStaticMethodID(env, mActivityClass, "isAndroidTV", "()Z"); + midIsChromebook = (*env)->GetStaticMethodID(env, mActivityClass, "isChromebook", "()Z"); + midIsDeXMode = (*env)->GetStaticMethodID(env, mActivityClass, "isDeXMode", "()Z"); + midIsTablet = (*env)->GetStaticMethodID(env, mActivityClass, "isTablet", "()Z"); + 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"); + midOpenFileDescriptor = (*env)->GetStaticMethodID(env, mActivityClass, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I"); + midManualBackButton = (*env)->GetStaticMethodID(env, mActivityClass, "manualBackButton", "()V"); +#ifndef SDL_DIALOG_DISABLED + midShowFileDialog = (*env)->GetStaticMethodID(env, mActivityClass, "showFileDialog", "([Ljava/lang/String;ZILjava/lang/String;I)Z"); +#endif // !SDL_DIALOG_DISABLED + midGetPreferredLocales = (*env)->GetStaticMethodID(env, mActivityClass, "getPreferredLocales", "()Ljava/lang/String;"); + + if (!midGetContext || + !midGetDeviceFormFactor || + !midGetManifestEnvironmentVariables || + !midIsAndroidTV || + !midIsChromebook || + !midIsDeXMode || + !midIsTablet || + !midOpenURL || + !midRequestPermission || + !midShowToast || + !midSendMessage || + !midOpenFileDescriptor || + !midManualBackButton || +#ifndef SDL_DIALOG_DISABLED + !midShowFileDialog || +#endif + !midGetPreferredLocales) { + __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some core Java callbacks, do you have the latest version of SDLActivity.java?"); + } + +#ifndef SDL_VIDEO_DISABLED + 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"); + midGetNativeSurface = (*env)->GetStaticMethodID(env, mActivityClass, "getNativeSurface", "()Landroid/view/Surface;"); + midInitTouch = (*env)->GetStaticMethodID(env, mActivityClass, "initTouch", "()V"); + midMinimizeWindow = (*env)->GetStaticMethodID(env, mActivityClass, "minimizeWindow", "()V"); + 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"); + + if (!midClipboardGetText || + !midClipboardHasText || + !midClipboardSetText || + !midCreateCustomCursor || + !midDestroyCustomCursor || + !midGetNativeSurface || + !midInitTouch || + !midMinimizeWindow || + !midSetActivityTitle || + !midSetCustomCursor || + !midSetOrientation || + !midSetRelativeMouseEnabled || + !midSetSystemCursor || + !midSetWindowStyle || + !midShouldMinimizeOnFocusLoss || + !midShowTextInput || + !midSupportsRelativeMouse) { + __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some video Java callbacks, do you have the latest version of SDLActivity.java?"); + } +#endif // !SDL_VIDEO_DISABLED - (*env)->ReleaseStringUTFChars(env, device_name, name); -#endif // SDL_HAPTIC_ANDROID + checkJNIReady(); } -JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic)( - JNIEnv *env, jclass jcls, jint device_id) +JNIEXPORT jint JNICALL SDL_JAVA_INTERFACE(nativeGetCompiledSubsystems)(JNIEnv *env, jclass jcls) { -#ifdef SDL_HAPTIC_ANDROID - Android_RemoveHaptic(device_id); + Uint32 subsystems = SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMEPAD | + SDL_INIT_SENSOR | SDL_INIT_CAMERA; +#ifdef SDL_AUDIO_DISABLED + subsystems &= ~SDL_INIT_AUDIO; #endif +#ifdef SDL_VIDEO_DISABLED + subsystems &= ~SDL_INIT_VIDEO; +#endif +#ifdef SDL_JOYSTICK_DISABLED + subsystems &= ~(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD); +#endif +#ifdef SDL_HAPTIC_DISABLED + subsystems &= ~SDL_INIT_HAPTIC; +#endif +#ifdef SDL_SENSOR_DISABLED + subsystems &= ~SDL_INIT_SENSOR; +#endif +#ifdef SDL_CAMERA_DISABLED + subsystems &= ~SDL_INIT_CAMERA; +#endif + return (jint)subsystems; } -#ifndef SDL_VIDEO_DISABLED -// Called from surfaceCreated() -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceCreated)(JNIEnv *env, jclass jcls) +#ifndef SDL_AUDIO_DISABLED +// Audio support +// Audio initialization -- called before SDL_main() to initialize JNI bindings +JNIEXPORT void JNICALL SDL_JAVA_AUDIO_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cls) { - SDL_LockMutex(Android_ActivityMutex); + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "AUDIO nativeSetupJNI()"); - if (Android_Window) { - SDL_WindowData *data = Android_Window->internal; + mAudioManagerClass = (jclass)((*env)->NewGlobalRef(env, cls)); - 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"); - } + 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?"); } - SDL_UnlockMutex(Android_ActivityMutex); + checkJNIReady(); } -// Called from surfaceChanged() -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceChanged)(JNIEnv *env, jclass jcls) +JNIEXPORT void JNICALL SDL_JAVA_AUDIO_INTERFACE(nativeAddAudioDevice)(JNIEnv *env, jclass jcls, jboolean recording, jstring name, jint device_id) { - 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); +#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); } - - // GL Context handling is done in the event loop because this function is run from the Java thread } #endif +} - if (Android_Window) { - Android_RestoreScreenKeyboard(SDL_GetVideoDevice(), Android_Window); +JNIEXPORT void JNICALL SDL_JAVA_AUDIO_INTERFACE(nativeRemoveAudioDevice)(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))); } - - SDL_UnlockMutex(Android_ActivityMutex); +#endif } -// Called from surfaceDestroyed() -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeSurfaceDestroyed)(JNIEnv *env, jclass jcls) + + +static void Android_JNI_AudioSetThreadPriority(int recording, int device_id) { - int nb_attempt = 50; + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mAudioManagerClass, midAudioSetThreadPriority, recording, device_id); +} -retry: +#endif // !SDL_AUDIO_DISABLED - SDL_LockMutex(Android_ActivityMutex); +#ifndef SDL_JOYSTICK_DISABLED +// 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()"); - if (Android_Window) { - SDL_WindowData *data = Android_Window->internal; + mControllerManagerClass = (jclass)((*env)->NewGlobalRef(env, cls)); - // 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; - } - } + midDetectDevices = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "detectDevices", "()V"); + midJoystickSetLED = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "joystickSetLED", "(IIII)V"); + midJoystickSetSensorsEnabled = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "joystickSetSensorsEnabled", "(IZ)V"); +#ifndef SDL_HAPTIC_DISABLED + midDetectHapticDevices = (*env)->GetStaticMethodID(env, mControllerManagerClass, + "detectHapticDevices", "()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"); +#endif // !SDL_HAPTIC_DISABLED -#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; - } + if (!midDetectDevices || !midJoystickSetLED || !midJoystickSetSensorsEnabled +#ifndef SDL_HAPTIC_DISABLED + || !midDetectHapticDevices || !midHapticRun || !midHapticRumble || !midHapticStop #endif + ) { + __android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some Java callbacks, do you have the latest version of SDLControllerManager.java?"); + } - if (data->native_window) { - ANativeWindow_release(data->native_window); - data->native_window = NULL; - } + checkJNIReady(); +} +#endif // !SDL_JOYSTICK_DISABLED - // GL Context handling is done in the event loop because this function is run from the Java thread - } +static int run_count = 0; +static bool allow_recreate_activity; +static bool allow_recreate_activity_set; - SDL_UnlockMutex(Android_ActivityMutex); +JNIEXPORT int JNICALL SDL_JAVA_INTERFACE(nativeCheckSDLThreadCounter)( + JNIEnv *env, jclass jcls) +{ + int tmp = run_count; + run_count += 1; + return tmp; } -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeScreenKeyboardShown)(JNIEnv *env, jclass jcls) + +JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(nativeAllowRecreateActivity)( + JNIEnv *env, jclass jcls) { - SDL_SendScreenKeyboardShown(); + return allow_recreate_activity; } -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeScreenKeyboardHidden)(JNIEnv *env, jclass jcls) +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeInitMainThread)( + JNIEnv *env, jclass jcls) { - SDL_SendScreenKeyboardHidden(); + __android_log_print(ANDROID_LOG_VERBOSE, "SDL", "nativeInitSDLThread() %d time", run_count); + run_count += 1; + + // Save JNIEnv of SDLThread + Android_JNI_SetEnv(env); } -// Keydown -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyDown)( - JNIEnv *env, jclass jcls, - jint keycode) +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeCleanupMainThread)( + JNIEnv *env, jclass jcls) { - SDL_LockMutex(Android_ActivityMutex); + /* 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) { + // Use the name "app_process" for argv[0] so PHYSFS_platformCalcBaseDir() works. + // https://github.com/love2d/love-android/issues/24 + // (note that PhysicsFS hasn't used argv on Android in a long time, but we'll keep this for compat at least for SDL3's lifetime. --ryan.) + const char *argv0 = "app_process"; + const int len = (*env)->GetArrayLength(env, array); // argv elements, not counting argv[0]. + + size_t total_alloc_len = (SDL_strlen(argv0) + 1) + ((len + 2) * sizeof (char *)); // len+2 to allocate an array that also holds argv0 and a NULL terminator. + for (int i = 0; i < len; ++i) { + total_alloc_len++; // null terminator. + jstring string = (*env)->GetObjectArrayElement(env, array, i); + if (string) { + const char *utf = (*env)->GetStringUTFChars(env, string, 0); + if (utf) { + total_alloc_len += SDL_strlen(utf) + 1; + (*env)->ReleaseStringUTFChars(env, string, utf); + } + (*env)->DeleteLocalRef(env, string); + } + } + + void *args = malloc(total_alloc_len); // This should NOT be SDL_malloc() + if (!args) { // uhoh. + __android_log_print(ANDROID_LOG_ERROR, "SDL", "nativeRunMain(): Out of memory parsing command line!"); + } else { + size_t remain = total_alloc_len - (sizeof (char *) * (len + 2)); + int argc = 0; + char **argv = (char **) args; + char *ptr = (char *) &argv[len + 2]; + size_t cpy = SDL_strlcpy(ptr, argv0, remain) + 1; + argv[argc++] = ptr; + SDL_assert(cpy <= remain); remain -= cpy; ptr += cpy; + for (int i = 0; i < len; ++i) { + jstring string = (*env)->GetObjectArrayElement(env, array, i); + const char *utf = string ? (*env)->GetStringUTFChars(env, string, 0) : NULL; + cpy = SDL_strlcpy(ptr, utf ? utf : "", remain) + 1; + if (cpy < remain) { + argv[argc++] = ptr; + remain -= cpy; + ptr += cpy; + } + if (utf) { + (*env)->ReleaseStringUTFChars(env, string, utf); + } + if (string) { + (*env)->DeleteLocalRef(env, string); + } + } + argv[argc] = NULL; + + // Run the application. + status = SDL_RunApp(argc, argv, SDL_main, NULL); + + // Release the arguments. + free(args); // This should NOT be SDL_free() + } + } 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); - if (Android_Window) { - Android_OnKeyDown(keycode); + 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); - SDL_UnlockMutex(Android_ActivityMutex); + // Do not issue an exit or the whole application will terminate instead of just the SDL thread + // exit(status); + + return status; } -// Keyup -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyUp)( - JNIEnv *env, jclass jcls, - jint keycode) +static int FindLifecycleEvent(SDL_AndroidLifecycleEvent event) { - SDL_LockMutex(Android_ActivityMutex); - - if (Android_Window) { - Android_OnKeyUp(keycode); + for (int index = 0; index < Android_NumLifecycleEvents; ++index) { + if (Android_LifecycleEvents[index] == event) { + return index; + } } - - SDL_UnlockMutex(Android_ActivityMutex); + return -1; } -// Virtual keyboard return key might stop text input -JNIEXPORT jboolean JNICALL SDL_JAVA_INTERFACE(onNativeSoftReturnKey)( - JNIEnv *env, jclass jcls) +static void RemoveLifecycleEvent(int index) { - if (SDL_GetHintBoolean(SDL_HINT_RETURN_KEY_HIDES_IME, false)) { - SDL_StopTextInput(Android_Window); - return JNI_TRUE; + if (index < Android_NumLifecycleEvents - 1) { + SDL_memmove(&Android_LifecycleEvents[index], &Android_LifecycleEvents[index+1], (Android_NumLifecycleEvents - index - 1) * sizeof(Android_LifecycleEvents[index])); } - return JNI_FALSE; + --Android_NumLifecycleEvents; } -// Keyboard Focus Lost -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeKeyboardFocusLost)( - JNIEnv *env, jclass jcls) +#ifndef SDL_JOYSTICK_DISABLED +// Paddown +JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadDown)( + JNIEnv *env, jclass jcls, + jint device_id, jint keycode, jint scancode) { - // Calling SDL_StopTextInput will take care of hiding the keyboard and cleaning up the DummyText widget - SDL_StopTextInput(Android_Window); +#ifdef SDL_JOYSTICK_ANDROID + return Android_OnPadDown(device_id, keycode, scancode); +#else + return false; +#endif // SDL_JOYSTICK_ANDROID } -// Touch -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeTouch)( +// Padup +JNIEXPORT jboolean JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativePadUp)( JNIEnv *env, jclass jcls, - jint touch_device_id_in, jint pointer_finger_id_in, - jint action, jfloat x, jfloat y, jfloat p) + jint device_id, jint keycode, jint scancode) { - SDL_LockMutex(Android_ActivityMutex); - - Android_OnTouch(Android_Window, touch_device_id_in, pointer_finger_id_in, action, x, y, p); - - SDL_UnlockMutex(Android_ActivityMutex); +#ifdef SDL_JOYSTICK_ANDROID + return Android_OnPadUp(device_id, keycode, scancode); +#else + return false; +#endif // SDL_JOYSTICK_ANDROID } -// Pinch -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePinchStart)( - JNIEnv *env, jclass jcls, jfloat span_x, jfloat span_y, jfloat focus_x, jfloat focus_y) +// Joy +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoy)( + JNIEnv *env, jclass jcls, + jint device_id, jint axis, jfloat value) { - SDL_LockMutex(Android_ActivityMutex); - - if (Android_Window) { - SDL_SendPinch(SDL_EVENT_PINCH_BEGIN, 0, Android_Window, 0, span_x, span_y, focus_x, focus_y); - } - - SDL_UnlockMutex(Android_ActivityMutex); +#ifdef SDL_JOYSTICK_ANDROID + Android_OnJoy(device_id, axis, value); +#endif // SDL_JOYSTICK_ANDROID } -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePinchUpdate)( - JNIEnv *env, jclass jcls, jfloat scale, jfloat span_x, jfloat span_y, jfloat focus_x, jfloat focus_y) +// POV Hat +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeHat)( + JNIEnv *env, jclass jcls, + jint device_id, jint hat_id, jint x, jint y) { - SDL_LockMutex(Android_ActivityMutex); - - if (Android_Window) { - SDL_SendPinch(SDL_EVENT_PINCH_UPDATE, 0, Android_Window, scale, span_x, span_y, focus_x, focus_y); - } - - SDL_UnlockMutex(Android_ActivityMutex); +#ifdef SDL_JOYSTICK_ANDROID + Android_OnHat(device_id, hat_id, x, y); +#endif // SDL_JOYSTICK_ANDROID } -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePinchEnd)( - JNIEnv *env, jclass jcls, jfloat span_x, jfloat span_y, jfloat focus_x, jfloat focus_y) +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(onNativeJoySensor)( + JNIEnv *env, jclass jcls, + jint device_id, jint sensor_type, jlong sensor_timestamp, jfloat x, jfloat y, jfloat z) { - SDL_LockMutex(Android_ActivityMutex); - - if (Android_Window) { - SDL_SendPinch(SDL_EVENT_PINCH_END, 0, Android_Window, 0, span_x, span_y, focus_x, focus_y); - } - - SDL_UnlockMutex(Android_ActivityMutex); +#ifdef SDL_JOYSTICK_ANDROID + // In Java there's no Uint64 type, so pass Sint64 as if it was Uint64. + Android_OnJoySensor(device_id, sensor_type, sensor_timestamp, x, y, z); +#endif // SDL_JOYSTICK_ANDROID } -// Mouse -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeMouse)( +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeAddJoystick)( JNIEnv *env, jclass jcls, - jint button, jint action, jfloat x, jfloat y, jboolean relative) + 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, jboolean has_rgb_led, + jboolean has_accelerometer, jboolean has_gyroscope) { - SDL_LockMutex(Android_ActivityMutex); +#ifdef SDL_JOYSTICK_ANDROID + const char *name = (*env)->GetStringUTFChars(env, device_name, NULL); + const char *desc = (*env)->GetStringUTFChars(env, device_desc, NULL); - Android_OnMouse(Android_Window, button, action, x, y, relative); + Android_AddJoystick(device_id, name, desc, vendor_id, product_id, button_mask, naxes, axis_mask, nhats, + can_rumble, has_rgb_led, has_accelerometer, has_gyroscope); - SDL_UnlockMutex(Android_ActivityMutex); + (*env)->ReleaseStringUTFChars(env, device_name, name); + (*env)->ReleaseStringUTFChars(env, device_desc, desc); +#endif // SDL_JOYSTICK_ANDROID } -// Pen -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativePen)( +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveJoystick)( JNIEnv *env, jclass jcls, - jint pen_id_in, jint device_type, jint button, jint action, jfloat x, jfloat y, jfloat p) + jint device_id) { - SDL_LockMutex(Android_ActivityMutex); +#ifdef SDL_JOYSTICK_ANDROID + Android_RemoveJoystick(device_id); +#endif // SDL_JOYSTICK_ANDROID +} +#endif // !SDL_JOYSTICK_DISABLED - Android_OnPen(Android_Window, pen_id_in, device_type, button, action, x, y, p); +#ifndef SDL_HAPTIC_DISABLED +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); - SDL_UnlockMutex(Android_ActivityMutex); + Android_AddHaptic(device_id, name); + + (*env)->ReleaseStringUTFChars(env, device_name, name); +#endif // SDL_HAPTIC_ANDROID } -// Clipboard -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeClipboardChanged)( - JNIEnv *env, jclass jcls) +JNIEXPORT void JNICALL SDL_JAVA_CONTROLLER_INTERFACE(nativeRemoveHaptic)( + JNIEnv *env, jclass jcls, jint device_id) { - // TODO: compute new mime types - SDL_SendClipboardUpdate(false, NULL, 0); +#ifdef SDL_HAPTIC_ANDROID + Android_RemoveHaptic(device_id); +#endif } -#endif // !SDL_VIDEO_DISABLED +#endif // !SDL_HAPTIC_DISABLED // Low memory JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeLowMemory)( @@ -1752,111 +1660,21 @@ static void LocalReferenceHolder_Cleanup(struct LocalReferenceHolder *refholder) JNIEnv *env = refholder->m_env; (*env)->PopLocalFrame(env, NULL); SDL_AtomicDecRef(&s_active); - } -} - -#ifndef SDL_VIDEO_DISABLED -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); -} - -#endif // !SDL_VIDEO_DISABLED - -// Outside the video guard: the camera driver reads these cached values (only written by the video Java layer). -SDL_DisplayOrientation Android_JNI_GetDisplayNaturalOrientation(void) -{ - return displayNaturalOrientation; -} - -SDL_DisplayOrientation Android_JNI_GetDisplayCurrentOrientation(void) -{ - return displayCurrentOrientation; -} - -#ifndef SDL_VIDEO_DISABLED -void Android_JNI_MinimizeWindow(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); -} - -#else -bool Android_JNI_ShouldMinimizeOnFocusLoss(void) -{ - return false; -} -#endif // !SDL_VIDEO_DISABLED - -#ifndef SDL_AUDIO_DISABLED -/* - * 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) +#ifdef SDL_VIDEO_DISABLED +bool Android_JNI_ShouldMinimizeOnFocusLoss(void) { - JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mAudioManagerClass, midUnregisterAudioDeviceCallback); + return false; } +#endif // SDL_VIDEO_DISABLED -static void Android_JNI_AudioSetThreadPriority(int recording, int device_id) +void SDL_SendAndroidBackButton(void) { 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); + (*env)->CallStaticVoidMethod(env, mActivityClass, midManualBackButton); } -#endif // !SDL_AUDIO_DISABLED // 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. @@ -2386,434 +2204,792 @@ static bool PrepareAPK(void) } LocalReferenceHolder_Cleanup(&refs); } - return retval; // even on failure, leave an empty root node so we have zero files and don't try to load the .zip again. + return retval; // even on failure, leave an empty root node so we have zero files and don't try to load the .zip again. +} + +static void Internal_Android_Create_AssetManager(void) +{ + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_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; + } + + if (APKRootNode) { + FreeAPKNode(APKRootNode); + APKRootNode = NULL; + } +} + +static const char *GetAssetPath(const char *path) +{ + if (!path) { + return NULL; + } + + if (path[0] == '.' && ((path[1] == '/') || (path[1] == '\0'))) { + path++; + } else if (SDL_strncmp(path, "assets://", 9) == 0) { + path += 9; + } + + while (*path == '/') { + ++path; + } + + return path; +} + + + + + +// See SDLActivity.java for constants. +#define COMMAND_SET_KEEP_SCREEN_ON 5 + +bool SDL_SendAndroidMessage(Uint32 command, int param) +{ + CHECK_PARAM(command < 0x8000) { + return SDL_InvalidParamError("command"); + } + return Android_JNI_SendMessage(command, param); +} + + +/* +////////////////////////////////////////////////////////////////////////////// +// +// 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); +} + + + + + + +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); +} + +const char *SDL_GetAndroidInternalStoragePath(void) +{ + static char *s_AndroidInternalFilesPath = NULL; + + if (!s_AndroidInternalFilesPath) { + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_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(SDL_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(SDL_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; } -static void Internal_Android_Create_AssetManager(void) +const char *SDL_GetAndroidCachePath(void) { - struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION); - JNIEnv *env = Android_JNI_GetEnv(); - jmethodID mid; - jobject context; - jobject javaAssetManager; + // !!! FIXME: lots of duplication with SDL_GetAndroidExternalStoragePath and SDL_GetAndroidInternalStoragePath; consolidate these functions! + static char *s_AndroidCachePath = NULL; - if (!LocalReferenceHolder_Init(&refs, env)) { - LocalReferenceHolder_Cleanup(&refs); - return; - } + if (!s_AndroidCachePath) { + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION); + jmethodID mid; + jobject context; + jobject fileObject; + jstring pathString; + const char *path; - // context = SDLActivity.getContext(); - context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); + JNIEnv *env = Android_JNI_GetEnv(); + if (!LocalReferenceHolder_Init(&refs, env)) { + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } - // javaAssetManager = context.getAssets(); - mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), - "getAssets", "()Landroid/content/res/AssetManager;"); - javaAssetManager = (*env)->CallObjectMethod(env, context, mid); + // context = SDLActivity.getContext(); + context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); - /** - * 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); + // fileObj = context.getExternalFilesDir(); + mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), + "getCacheDir", "()Ljava/io/File;"); + fileObject = (*env)->CallObjectMethod(env, context, mid); + if (!fileObject) { + SDL_SetError("Couldn't get cache directory"); + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } - if (!asset_manager) { - (*env)->DeleteGlobalRef(env, javaAssetManagerRef); - Android_JNI_ExceptionOccurred(true); + // 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; +} - LocalReferenceHolder_Cleanup(&refs); +bool SDL_ShowAndroidToast(const char *message, int duration, int gravity, int xOffset, int yOffset) +{ + return Android_JNI_ShowToast(message, duration, gravity, xOffset, yOffset); } -static void Internal_Android_Destroy_AssetManager(void) + +typedef struct NativePermissionRequestInfo { - JNIEnv *env = Android_JNI_GetEnv(); + int request_code; + char *permission; + SDL_RequestAndroidPermissionCallback callback; + void *userdata; + struct NativePermissionRequestInfo *next; +} NativePermissionRequestInfo; - if (asset_manager) { - (*env)->DeleteGlobalRef(env, javaAssetManagerRef); - asset_manager = NULL; - } +static NativePermissionRequestInfo pending_permissions; - if (APKRootNode) { - FreeAPKNode(APKRootNode); - APKRootNode = NULL; +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePermissionResult)( + JNIEnv *env, jclass cls, + jint requestCode, jboolean result) +{ + SDL_LockMutex(SDL_event_lock); + 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(SDL_event_lock); + info->callback(info->userdata, info->permission, result ? true : false); + SDL_free(info->permission); + SDL_free(info); + return; + } + prev = info; } + + SDL_UnlockMutex(SDL_event_lock); } -static const char *GetAssetPath(const char *path) +bool SDL_RequestAndroidPermission(const char *permission, SDL_RequestAndroidPermissionCallback cb, void *userdata) { - if (!path) { - return NULL; + if (!permission) { + return SDL_InvalidParamError("permission"); + } else if (!cb) { + return SDL_InvalidParamError("cb"); } - if (path[0] == '.' && ((path[1] == '/') || (path[1] == '\0'))) { - path++; - } else if (SDL_strncmp(path, "assets://", 9) == 0) { - path += 9; + NativePermissionRequestInfo *info = (NativePermissionRequestInfo *) SDL_calloc(1, sizeof (NativePermissionRequestInfo)); + if (!info) { + return false; } - while (*path == '/') { - ++path; + info->permission = SDL_strdup(permission); + if (!info->permission) { + SDL_free(info); + return false; } - return path; -} - -bool Android_JNI_FileOpen(void **puserdata, const char *fileName, const char *mode) -{ - SDL_assert(puserdata != NULL); - - AAsset *asset = NULL; - *puserdata = NULL; + static SDL_AtomicInt next_request_code; + info->request_code = SDL_AddAtomicInt(&next_request_code, 1); - if (!asset_manager) { - Internal_Android_Create_AssetManager(); - if (!asset_manager) { - return SDL_SetError("Couldn't create asset manager"); - } - } + info->callback = cb; + info->userdata = userdata; - fileName = GetAssetPath(fileName); + SDL_LockMutex(SDL_event_lock); + info->next = pending_permissions.next; + pending_permissions.next = info; + SDL_UnlockMutex(SDL_event_lock); - asset = AAssetManager_open(asset_manager, fileName, AASSET_MODE_UNKNOWN); - if (!asset) { - return SDL_SetError("Couldn't open asset '%s'", fileName); - } + JNIEnv *env = Android_JNI_GetEnv(); + jstring jpermission = (*env)->NewStringUTF(env, permission); + (*env)->CallStaticVoidMethod(env, mActivityClass, midRequestPermission, jpermission, info->request_code); + (*env)->DeleteLocalRef(env, jpermission); - *puserdata = (void *)asset; return true; } -size_t Android_JNI_FileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status) +#ifndef SDL_DIALOG_DISABLED +static struct AndroidFileDialog { - const int bytes = AAsset_read((AAsset *)userdata, buffer, size); - if (bytes < 0) { - SDL_SetError("AAsset_read() failed"); - *status = SDL_IO_STATUS_ERROR; - return 0; - } else if (bytes < size) { - *status = SDL_IO_STATUS_EOF; - } - return (size_t)bytes; -} + int request_code; + SDL_DialogFileCallback callback; + void *userdata; +} mAndroidFileDialogData; -size_t Android_JNI_FileWrite(void *userdata, const void *buffer, size_t size, SDL_IOStatus *status) +JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeFileDialog)( + JNIEnv *env, jclass jcls, + jint requestCode, jobjectArray fileList, jint filter) { - SDL_SetError("Cannot write to Android package filesystem"); - *status = SDL_IO_STATUS_ERROR; - return 0; -} + 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; + } -Sint64 Android_JNI_FileSize(void *userdata) -{ - return (Sint64) AAsset_getLength64((AAsset *)userdata); -} + // Convert fileList to string + size_t count = (*env)->GetArrayLength(env, fileList); + char **charFileList = SDL_calloc(count + 1, sizeof(char *)); -Sint64 Android_JNI_FileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence) -{ - return (Sint64) AAsset_seek64((AAsset *)userdata, offset, (int)whence); -} + if (charFileList == NULL) { + mAndroidFileDialogData.callback(mAndroidFileDialogData.userdata, NULL, -1); + mAndroidFileDialogData.callback = NULL; + return; + } -bool Android_JNI_FileClose(void *userdata) -{ - AAsset_close((AAsset *)userdata); - return true; -} + // 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; + } -bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata) -{ - SDL_assert(path != NULL); + const char *utf8string = (*env)->GetStringUTFChars(env, string, NULL); + if (!utf8string) { + (*env)->DeleteLocalRef(env, string); + continue; + } - if (!PrepareAPK()) { - return false; - } + 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; - SDL_EnumerationResult result = SDL_ENUM_CONTINUE; - const char *asset_path = GetAssetPath(path); + // Cleanup memory + for (int j = 0; j < i; j++) { + SDL_free(charFileList[j]); + } + SDL_free(charFileList); + return; + } - const APKNode *apknode = FindAPKNode(asset_path); - if (!apknode) { - return SDL_SetError("No such directory"); - } else if (apknode->info.type != SDL_PATHTYPE_DIRECTORY) { - return SDL_SetError("Not a directory"); - } else { - for (const APKNode *node = apknode->children; node && (result == SDL_ENUM_CONTINUE); node = node->next_sibling) { - result = cb(userdata, path, node->name); + charFileList[i] = newFile; + (*env)->ReleaseStringUTFChars(env, string, utf8string); + (*env)->DeleteLocalRef(env, string); } - } - - return (result != SDL_ENUM_FAILURE); -} - -bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info) -{ - SDL_assert(path != NULL); - if (!PrepareAPK()) { - return false; - } + // Call user-provided callback + SDL_ClearError(); + mAndroidFileDialogData.callback(mAndroidFileDialogData.userdata, (const char *const *) charFileList, filter); + mAndroidFileDialogData.callback = NULL; - path = GetAssetPath(path); - const APKNode *apknode = FindAPKNode(path); - if (!apknode) { - return SDL_SetError("No such file or directory"); + // Cleanup memory + for (int i = 0; i < count; i++) { + SDL_free(charFileList[i]); + } + SDL_free(charFileList); } - SDL_copyp(info, &apknode->info); - return true; } +#endif // !SDL_DIALOG_DISABLED -#ifndef SDL_VIDEO_DISABLED -bool Android_JNI_SetClipboardText(const char *text) +/******************************************************************************* + Interface functions (declared in SDL_android.h) + Ordered to match SDL_android.h. +*******************************************************************************/ + +#ifndef SDL_AUDIO_DISABLED +// Audio support +void Android_StartAudioHotplug(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) { JNIEnv *env = Android_JNI_GetEnv(); - jstring string = (*env)->NewStringUTF(env, text); - (*env)->CallStaticVoidMethod(env, mActivityClass, midClipboardSetText, string); - (*env)->DeleteLocalRef(env, string); - return true; + // 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? } -char *Android_JNI_GetClipboardText(void) +void Android_StopAudioHotplug(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; + (*env)->CallStaticVoidMethod(env, mAudioManagerClass, midUnregisterAudioDeviceCallback); } -bool Android_JNI_HasClipboardText(void) +void Android_AudioThreadInit(SDL_AudioDevice *device) { - JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midClipboardHasText); + Android_JNI_AudioSetThreadPriority((int) device->recording, (int)device->instance_id); } -#endif // !SDL_VIDEO_DISABLED +#endif // !SDL_AUDIO_DISABLED -/* 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) +// Life cycle +void Android_SendLifecycleEvent(SDL_AndroidLifecycleEvent event) { - struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_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); + SDL_LockMutex(Android_LifecycleMutex); + { + int index; + bool add_event = true; - 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; + 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; } - // 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; + if (add_event) { + SDL_assert(Android_NumLifecycleEvents < SDL_arraysize(Android_LifecycleEvents)); + Android_LifecycleEvents[Android_NumLifecycleEvents++] = event; + SDL_SignalSemaphore(Android_LifecycleEventSem); } - // 5 == BatteryManager.BATTERY_STATUS_FULL - *charged = (status == 5) ? 1 : 0; } + SDL_UnlockMutex(Android_LifecycleMutex); +} - if (battery) { - GET_BOOL_EXTRA(present, "present") // == BatteryManager.EXTRA_PRESENT (API 5) - *battery = present ? 1 : 0; - } +bool Android_WaitLifecycleEvent(SDL_AndroidLifecycleEvent *event, Sint64 timeoutNS) +{ + bool got_event = false; - if (seconds) { - *seconds = -1; // not possible + int relock_count = 0; + Android_LockActivityMutex(); + while (Android_ActivityMutexCount > 1) { + // We came into this function with the activity lock held, we need to unlock so lifecycle events can be dispatched + ++relock_count; + Android_UnlockActivityMutex(); } + Android_UnlockActivityMutex(); - 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 + while (!got_event && SDL_WaitSemaphoreTimeoutNS(Android_LifecycleEventSem, timeoutNS)) { + SDL_LockMutex(Android_LifecycleMutex); { - GET_INT_EXTRA(scale_temp, "scale") // == BatteryManager.EXTRA_SCALE (API 5) - scale = scale_temp; + if (Android_NumLifecycleEvents > 0) { + *event = Android_LifecycleEvents[0]; + RemoveLifecycleEvent(0); + got_event = true; + } } + SDL_UnlockMutex(Android_LifecycleMutex); + } - if ((level == -1) || (scale == -1)) { - LocalReferenceHolder_Cleanup(&refs); - return -1; - } - *percent = level * 100 / scale; + while (relock_count > 0) { + Android_LockActivityMutex(); + --relock_count; } + return got_event; +} - (*env)->DeleteLocalRef(env, intent); +void Android_LockActivityMutex(void) +{ + SDL_LockMutex(Android_ActivityMutex); + ++Android_ActivityMutexCount; +} - LocalReferenceHolder_Cleanup(&refs); - return 0; +void Android_UnlockActivityMutex(void) +{ + --Android_ActivityMutexCount; + SDL_UnlockMutex(Android_ActivityMutex); +} + +void Android_SetAllowRecreateActivity(bool enabled) +{ + allow_recreate_activity = enabled; + allow_recreate_activity_set = true; } #ifndef SDL_VIDEO_DISABLED -// Add all touch devices -void Android_JNI_InitTouch(void) +// Interface from the SDL library into the Android Java activity +void Android_JNI_SetActivityTitle(const char *title) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mActivityClass, midInitTouch); + + jstring jtitle = (*env)->NewStringUTF(env, title); + (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetActivityTitle, jtitle); + (*env)->DeleteLocalRef(env, jtitle); } -#endif // !SDL_VIDEO_DISABLED -void Android_JNI_DetectDevices(void) +void Android_JNI_SetWindowStyle(bool fullscreen) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midDetectDevices); + (*env)->CallStaticVoidMethod(env, mActivityClass, midSetWindowStyle, fullscreen ? 1 : 0); } -void Android_JNI_JoystickSetLED(int device_id, int red, int green, int blue) +void Android_JNI_SetOrientation(int w, int h, int resizable, const char *hint) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midJoystickSetLED, device_id, red, green, blue); + + jstring jhint = (*env)->NewStringUTF(env, (hint ? hint : "")); + (*env)->CallStaticVoidMethod(env, mActivityClass, midSetOrientation, w, h, (resizable ? 1 : 0), jhint); + (*env)->DeleteLocalRef(env, jhint); } -void Android_JNI_JoystickSetSensorsEnabled(int device_id, bool enabled) +void Android_JNI_MinimizeWindow(void) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midJoystickSetSensorsEnabled, device_id, (enabled == 1)); + (*env)->CallStaticVoidMethod(env, mActivityClass, midMinimizeWindow); } -void Android_JNI_DetectHapticDevices(void) +bool Android_JNI_ShouldMinimizeOnFocusLoss(void) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midDetectHapticDevices); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midShouldMinimizeOnFocusLoss); } -void Android_JNI_HapticRun(int device_id, float intensity, int length) +void Android_JNI_ShowScreenKeyboard(int input_type, SDL_Rect *inputRect) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticRun, device_id, intensity, length); + (*env)->CallStaticBooleanMethod(env, mActivityClass, midShowTextInput, + input_type, + inputRect->x, + inputRect->y, + inputRect->w, + inputRect->h); } -void Android_JNI_HapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) +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_SuspendScreenSaver(bool suspend) +{ + return Android_JNI_SendMessage(COMMAND_SET_KEEP_SCREEN_ON, (suspend == false) ? 0 : 1); +} + +ANativeWindow *Android_JNI_GetNativeWindow(void) { + ANativeWindow *anw = NULL; + jobject s; JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticRumble, device_id, low_frequency_intensity, high_frequency_intensity, length); + + s = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetNativeSurface); + if (s) { + anw = ANativeWindow_fromSurface(env, s); + (*env)->DeleteLocalRef(env, s); + } + + return anw; } -void Android_JNI_HapticStop(int device_id) +SDL_DisplayOrientation Android_JNI_GetDisplayNaturalOrientation(void) +{ + return displayNaturalOrientation; +} + +SDL_DisplayOrientation Android_JNI_GetDisplayCurrentOrientation(void) +{ + return displayCurrentOrientation; +} + +// Clipboard support +bool Android_JNI_SetClipboardText(const char *text) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticStop, device_id); + jstring string = (*env)->NewStringUTF(env, text); + (*env)->CallStaticVoidMethod(env, mActivityClass, midClipboardSetText, string); + (*env)->DeleteLocalRef(env, string); + return true; } -// See SDLActivity.java for constants. -#define COMMAND_SET_KEEP_SCREEN_ON 5 +char *Android_JNI_GetClipboardText(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + char *text = NULL; + jstring string; -bool SDL_SendAndroidMessage(Uint32 command, int param) + 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) { - CHECK_PARAM(command < 0x8000) { - return SDL_InvalidParamError("command"); + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midClipboardHasText); +} + +// Touch support +void Android_JNI_InitTouch(void) +{ + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mActivityClass, midInitTouch); +} + +// Cursor support +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 Android_JNI_SendMessage(command, param); + return custom_cursor; } -// sends message to be handled on the UI event dispatch thread -bool Android_JNI_SendMessage(int command, int param) +void Android_JNI_DestroyCustomCursor(int cursorID) { JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSendMessage, command, param); + (*env)->CallStaticVoidMethod(env, mActivityClass, midDestroyCustomCursor, cursorID); } -bool Android_JNI_SuspendScreenSaver(bool suspend) +bool Android_JNI_SetCustomCursor(int cursorID) { - return Android_JNI_SendMessage(COMMAND_SET_KEEP_SCREEN_ON, (suspend == false) ? 0 : 1); + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetCustomCursor, cursorID); } -#ifndef SDL_VIDEO_DISABLED -void Android_JNI_ShowScreenKeyboard(int input_type, SDL_Rect *inputRect) +bool Android_JNI_SetSystemCursor(int cursorID) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticBooleanMethod(env, mActivityClass, midShowTextInput, - input_type, - inputRect->x, - inputRect->y, - inputRect->w, - inputRect->h); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetSystemCursor, cursorID); } -void Android_JNI_HideScreenKeyboard(void) +// Relative mouse support +bool Android_JNI_SupportsRelativeMouse(void) { - // has to match Activity constant - const int COMMAND_TEXTEDIT_HIDE = 3; - Android_JNI_SendMessage(COMMAND_TEXTEDIT_HIDE, 0); + 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)); } -#endif // !SDL_VIDEO_DISABLED +// MessageBox bool Android_JNI_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID) { JNIEnv *env; @@ -2906,448 +3082,412 @@ bool Android_JNI_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *b return true; } +#endif // !SDL_VIDEO_DISABLED -/* -////////////////////////////////////////////////////////////////////////////// -// -// 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; -} - -char *SDL_GetAndroidPackageName(void) +bool Android_JNI_FileOpen(void **puserdata, const char *fileName, const char *mode) { - // this doesn't currently cache this, because it's only used by SDL_GetExeName, which _does_ cache it. - struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION); - - JNIEnv *env = Android_JNI_GetEnv(); - if (!LocalReferenceHolder_Init(&refs, env)) { - LocalReferenceHolder_Cleanup(&refs); - return NULL; - } - - // context = SDLActivity.getContext(); - jobject context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); - if (!context) { - SDL_SetError("Couldn't get Android context!"); - LocalReferenceHolder_Cleanup(&refs); - return NULL; - } + SDL_assert(puserdata != NULL); - // fileObj = context.getFilesDir(); - jmethodID mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), "getPackageName", "()Ljava/lang/String;"); - jstring jstr = (jstring)(*env)->CallObjectMethod(env, context, mid); - if (Android_JNI_ExceptionOccurred(false)) { - LocalReferenceHolder_Cleanup(&refs); - return NULL; + AAsset *asset = NULL; + *puserdata = NULL; + + if (!asset_manager) { + Internal_Android_Create_AssetManager(); + if (!asset_manager) { + return SDL_SetError("Couldn't create asset manager"); + } } - const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL); - char *retval = cstr ? SDL_strdup(cstr) : NULL; - (*env)->ReleaseStringUTFChars(env, jstr, cstr); + fileName = GetAssetPath(fileName); - LocalReferenceHolder_Cleanup(&refs); + asset = AAssetManager_open(asset_manager, fileName, AASSET_MODE_UNKNOWN); + if (!asset) { + return SDL_SetError("Couldn't open asset '%s'", fileName); + } - return retval; + *puserdata = (void *)asset; + return true; } +Sint64 Android_JNI_FileSize(void *userdata) +{ + return (Sint64) AAsset_getLength64((AAsset *)userdata); +} -bool SDL_IsAndroidTablet(void) +Sint64 Android_JNI_FileSeek(void *userdata, Sint64 offset, SDL_IOWhence whence) { - JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsTablet); + return (Sint64) AAsset_seek64((AAsset *)userdata, offset, (int)whence); } -bool SDL_IsAndroidTV(void) +size_t Android_JNI_FileRead(void *userdata, void *buffer, size_t size, SDL_IOStatus *status) { - JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsAndroidTV); + const int bytes = AAsset_read((AAsset *)userdata, buffer, size); + if (bytes < 0) { + SDL_SetError("AAsset_read() failed"); + *status = SDL_IO_STATUS_ERROR; + return 0; + } else if (bytes < size) { + *status = SDL_IO_STATUS_EOF; + } + return (size_t)bytes; } -bool SDL_IsChromebook(void) +size_t Android_JNI_FileWrite(void *userdata, const void *buffer, size_t size, SDL_IOStatus *status) { - JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsChromebook); + SDL_SetError("Cannot write to Android package filesystem"); + *status = SDL_IO_STATUS_ERROR; + return 0; } -bool SDL_IsDeXMode(void) +bool Android_JNI_FileClose(void *userdata) { - JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsDeXMode); + AAsset_close((AAsset *)userdata); + return true; } -void SDL_SendAndroidBackButton(void) +bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata) { -#ifndef SDL_VIDEO_DISABLED - JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mActivityClass, midManualBackButton); -#endif + SDL_assert(path != NULL); + + if (!PrepareAPK()) { + return false; + } + + SDL_EnumerationResult result = SDL_ENUM_CONTINUE; + const char *asset_path = GetAssetPath(path); + + const APKNode *apknode = FindAPKNode(asset_path); + if (!apknode) { + return SDL_SetError("No such directory"); + } else if (apknode->info.type != SDL_PATHTYPE_DIRECTORY) { + return SDL_SetError("Not a directory"); + } else { + for (const APKNode *node = apknode->children; node && (result == SDL_ENUM_CONTINUE); node = node->next_sibling) { + result = cb(userdata, path, node->name); + } + } + + return (result != SDL_ENUM_FAILURE); } -const char *SDL_GetAndroidInternalStoragePath(void) +bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info) { - static char *s_AndroidInternalFilesPath = NULL; + SDL_assert(path != NULL); - if (!s_AndroidInternalFilesPath) { - struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION); - jmethodID mid; - jobject context; - jobject fileObject; - jstring pathString; - const char *path; + if (!PrepareAPK()) { + return false; + } + + path = GetAssetPath(path); + const APKNode *apknode = FindAPKNode(path); + if (!apknode) { + return SDL_SetError("No such file or directory"); + } + SDL_copyp(info, &apknode->info); + return true; +} + +// Environment support +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(); - if (!LocalReferenceHolder_Init(&refs, env)) { - LocalReferenceHolder_Cleanup(&refs); - return NULL; + bool ret = (*env)->CallStaticBooleanMethod(env, mActivityClass, midGetManifestEnvironmentVariables); + if (ret) { + bHasEnvironmentVariables = true; } + } +} - // context = SDLActivity.getContext(); - context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); - if (!context) { - SDL_SetError("Couldn't get Android context!"); - LocalReferenceHolder_Cleanup(&refs); - return NULL; - } +int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode) +{ + // Get fopen-style modes + int moderead = 0, modewrite = 0, modeappend = 0, modeupdate = 0; - // 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; + 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; } + } - // 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; + // 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"; + } - path = (*env)->GetStringUTFChars(env, pathString, NULL); - s_AndroidInternalFilesPath = SDL_strdup(path); - (*env)->ReleaseStringUTFChars(env, pathString, path); + 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); - LocalReferenceHolder_Cleanup(&refs); + if (fd == -1) { + SDL_SetError("Unspecified error in JNI"); } - return s_AndroidInternalFilesPath; + + return fd; } -Uint32 SDL_GetAndroidExternalStorageState(void) +#ifndef SDL_POWER_DISABLED +/* 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(SDL_FUNCTION); + JNIEnv *env = Android_JNI_GetEnv(); jmethodID mid; + jobject context; + jstring action; jclass cls; - jstring stateString; - const char *state_string; - Uint32 stateFlags; - - JNIEnv *env = Android_JNI_GetEnv(); + jobject filter; + jobject intent; + jstring iname; + jmethodID imid; + jstring bname; + jmethodID bmid; if (!LocalReferenceHolder_Init(&refs, env)) { LocalReferenceHolder_Cleanup(&refs); - return 0; + return -1; } - cls = (*env)->FindClass(env, "android/os/Environment"); - mid = (*env)->GetStaticMethodID(env, cls, - "getExternalStorageState", "()Ljava/lang/String;"); - stateString = (jstring)(*env)->CallStaticObjectMethod(env, cls, mid); + // context = SDLActivity.getContext(); + context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); - state_string = (*env)->GetStringUTFChars(env, stateString, NULL); + action = (*env)->NewStringUTF(env, "android.intent.action.BATTERY_CHANGED"); - // Print an info message so people debugging know the storage state - __android_log_print(ANDROID_LOG_INFO, "SDL", "external storage state: %s", state_string); + cls = (*env)->FindClass(env, "android/content/IntentFilter"); - 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); + mid = (*env)->GetMethodID(env, cls, "", "(Ljava/lang/String;)V"); + filter = (*env)->NewObject(env, cls, mid, action); - LocalReferenceHolder_Cleanup(&refs); + (*env)->DeleteLocalRef(env, action); - return stateFlags; -} + mid = (*env)->GetMethodID(env, mActivityClass, "registerReceiver", "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;"); + intent = (*env)->CallObjectMethod(env, context, mid, NULL, filter); -const char *SDL_GetAndroidExternalStoragePath(void) -{ - static char *s_AndroidExternalFilesPath = NULL; + (*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); - if (!s_AndroidExternalFilesPath) { - struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION); - jmethodID mid; - jobject context; - jobject fileObject; - jstring pathString; - const char *path; + bmid = (*env)->GetMethodID(env, cls, "getBooleanExtra", "(Ljava/lang/String;Z)Z"); - JNIEnv *env = Android_JNI_GetEnv(); - if (!LocalReferenceHolder_Init(&refs, env)) { + // 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 NULL; + return -1; } + // 1 == BatteryManager.BATTERY_PLUGGED_AC + // 2 == BatteryManager.BATTERY_PLUGGED_USB + *plugged = (0 < plug) ? 1 : 0; + } - // 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"); + 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 NULL; + return -1; } + // 5 == BatteryManager.BATTERY_STATUS_FULL + *charged = (status == 5) ? 1 : 0; + } - // 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); + if (battery) { + GET_BOOL_EXTRA(present, "present") // == BatteryManager.EXTRA_PRESENT (API 5) + *battery = present ? 1 : 0; } - 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 (seconds) { + *seconds = -1; // not possible + } - if (!s_AndroidCachePath) { - struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION); - jmethodID mid; - jobject context; - jobject fileObject; - jstring pathString; - const char *path; + if (percent) { + int level; + int scale; - JNIEnv *env = Android_JNI_GetEnv(); - if (!LocalReferenceHolder_Init(&refs, env)) { - LocalReferenceHolder_Cleanup(&refs); - return NULL; + // 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; } - // 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); - if (!fileObject) { - SDL_SetError("Couldn't get cache directory"); + if ((level == -1) || (scale == -1)) { LocalReferenceHolder_Cleanup(&refs); - return NULL; + return -1; } - - // 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); + *percent = level * 100 / scale; } - 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); + (*env)->DeleteLocalRef(env, intent); + + LocalReferenceHolder_Cleanup(&refs); + return 0; } +#endif // !SDL_POWER_DISABLED -void Android_JNI_GetManifestEnvironmentVariables(void) +#ifndef SDL_JOYSTICK_DISABLED +void Android_JNI_DetectDevices(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; - } - } + JNIEnv *env = Android_JNI_GetEnv(); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midDetectDevices); } -#ifndef SDL_VIDEO_DISABLED -int Android_JNI_CreateCustomCursor(SDL_Surface *surface, int hot_x, int hot_y) +void Android_JNI_JoystickSetLED(int device_id, int red, int green, int blue) { 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; + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midJoystickSetLED, device_id, red, green, blue); } -void Android_JNI_DestroyCustomCursor(int cursorID) +void Android_JNI_JoystickSetSensorsEnabled(int device_id, bool enabled) { JNIEnv *env = Android_JNI_GetEnv(); - (*env)->CallStaticVoidMethod(env, mActivityClass, midDestroyCustomCursor, cursorID); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midJoystickSetSensorsEnabled, device_id, (enabled == 1)); } +#endif // !SDL_JOYSTICK_DISABLED -bool Android_JNI_SetCustomCursor(int cursorID) +#ifndef SDL_HAPTIC_DISABLED +void Android_JNI_DetectHapticDevices(void) { JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetCustomCursor, cursorID); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midDetectHapticDevices); } -bool Android_JNI_SetSystemCursor(int cursorID) +void Android_JNI_HapticRun(int device_id, float intensity, int length) { JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetSystemCursor, cursorID); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticRun, device_id, intensity, length); } -bool Android_JNI_SupportsRelativeMouse(void) +void Android_JNI_HapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSupportsRelativeMouse); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticRumble, device_id, low_frequency_intensity, high_frequency_intensity, length); } -bool Android_JNI_SetRelativeMouseEnabled(bool enabled) +void Android_JNI_HapticStop(int device_id) { JNIEnv *env = Android_JNI_GetEnv(); - return (*env)->CallStaticBooleanMethod(env, mActivityClass, midSetRelativeMouseEnabled, (enabled == 1)); + (*env)->CallStaticVoidMethod(env, mControllerManagerClass, midHapticStop, device_id); } -#endif // !SDL_VIDEO_DISABLED +#endif // !SDL_HAPTIC_DISABLED -typedef struct NativePermissionRequestInfo +// Threads +// Get local storage value +JNIEnv *Android_JNI_GetEnv(void) { - int request_code; - char *permission; - SDL_RequestAndroidPermissionCallback callback; - void *userdata; - struct NativePermissionRequestInfo *next; -} NativePermissionRequestInfo; + // 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; -static NativePermissionRequestInfo pending_permissions; + // There should be a JVM + if (!mJavaVM) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed, there is no JavaVM"); + return NULL; + } -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativePermissionResult)( - JNIEnv *env, jclass cls, - jint requestCode, jboolean result) -{ - SDL_LockMutex(SDL_event_lock); - 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(SDL_event_lock); - info->callback(info->userdata, info->permission, result ? true : false); - SDL_free(info->permission); - SDL_free(info); - return; + /* 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; } - prev = info; } - SDL_UnlockMutex(SDL_event_lock); + return env; } -bool SDL_RequestAndroidPermission(const char *permission, SDL_RequestAndroidPermissionCallback cb, void *userdata) +// Set up an external thread for using JNI with Android_JNI_GetEnv() +bool Android_JNI_SetupThread(void) { - 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; - } + JNIEnv *env; + int status; - info->permission = SDL_strdup(permission); - if (!info->permission) { - SDL_free(info); + // There should be a JVM + if (!mJavaVM) { + __android_log_print(ANDROID_LOG_ERROR, "SDL", "Failed, there is no JavaVM"); 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(SDL_event_lock); - info->next = pending_permissions.next; - pending_permissions.next = info; - SDL_UnlockMutex(SDL_event_lock); + /* 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; + } - JNIEnv *env = Android_JNI_GetEnv(); - jstring jpermission = (*env)->NewStringUTF(env, permission); - (*env)->CallStaticVoidMethod(env, mActivityClass, midRequestPermission, jpermission, info->request_code); - (*env)->DeleteLocalRef(env, jpermission); + // Save JNIEnv into the Thread local storage + if (!Android_JNI_SetEnv(env)) { + return false; + } 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; -} - +// Locale bool Android_JNI_GetLocale(char *buf, size_t buflen) { bool result = false; @@ -3368,6 +3508,25 @@ bool Android_JNI_GetLocale(char *buf, size_t buflen) return result; } +// Generic messages +// 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); +} + +// 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_OpenURL(const char *url) { bool result; @@ -3378,136 +3537,100 @@ bool Android_JNI_OpenURL(const char *url) return result; } -int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode) +int SDL_GetAndroidSDKVersion(void) { - // 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"; + 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); } - } else if (modewrite) { - contentResolverMode = modeupdate ? "rwt" : "wt"; - } else if (modeappend) { - contentResolverMode = modeupdate ? "rw" : "wa"; } + return sdk_version; +} +bool SDL_IsAndroidTablet(void) +{ 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; + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsTablet); } -static struct AndroidFileDialog +bool SDL_IsAndroidTV(void) { - int request_code; - SDL_DialogFileCallback callback; - void *userdata; -} mAndroidFileDialogData; + JNIEnv *env = Android_JNI_GetEnv(); + return (*env)->CallStaticBooleanMethod(env, mActivityClass, midIsAndroidTV); +} -JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeFileDialog)( - JNIEnv *env, jclass jcls, - jint requestCode, jobjectArray fileList, jint filter) +SDL_FormFactor SDL_GetAndroidDeviceFormFactor(void) { - 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 *)); + JNIEnv *env = Android_JNI_GetEnv(); + SDL_FormFactor form_factor = SDL_FORMFACTOR_UNKNOWN; + jstring string; - if (charFileList == NULL) { - mAndroidFileDialogData.callback(mAndroidFileDialogData.userdata, NULL, -1); - mAndroidFileDialogData.callback = NULL; - return; + string = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetDeviceFormFactor); + if (string) { + const char *utf = (*env)->GetStringUTFChars(env, string, 0); + if (utf) { + if (SDL_strcmp(utf, "tv") == 0) { + form_factor = SDL_FORMFACTOR_TV; + } else if (SDL_strcmp(utf, "tablet") == 0) { + form_factor = SDL_FORMFACTOR_TABLET; + } else if (SDL_strcmp(utf, "phone") == 0) { + form_factor = SDL_FORMFACTOR_PHONE; + } else if (SDL_strcmp(utf, "car") == 0) { + form_factor = SDL_FORMFACTOR_CAR; + } else if (SDL_strcmp(utf, "headset") == 0) { + form_factor = SDL_FORMFACTOR_HEADSET; + } else if (SDL_strcmp(utf, "watch") == 0) { + form_factor = SDL_FORMFACTOR_WATCH; + } else { + form_factor = SDL_FORMFACTOR_UNKNOWN; + } + (*env)->ReleaseStringUTFChars(env, string, utf); } + (*env)->DeleteLocalRef(env, string); + } - // 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; - } + return form_factor; +} - const char *utf8string = (*env)->GetStringUTFChars(env, string, NULL); - if (!utf8string) { - (*env)->DeleteLocalRef(env, string); - continue; - } +char *SDL_GetAndroidPackageName(void) +{ + // this doesn't currently cache this, because it's only used by SDL_GetExeName, which _does_ cache it. + struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION); - 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; + JNIEnv *env = Android_JNI_GetEnv(); + if (!LocalReferenceHolder_Init(&refs, env)) { + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } - // Cleanup memory - for (int j = 0; j < i; j++) { - SDL_free(charFileList[j]); - } - SDL_free(charFileList); - return; - } + // context = SDLActivity.getContext(); + jobject context = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetContext); + if (!context) { + SDL_SetError("Couldn't get Android context!"); + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } - charFileList[i] = newFile; - (*env)->ReleaseStringUTFChars(env, string, utf8string); - (*env)->DeleteLocalRef(env, string); - } + // fileObj = context.getFilesDir(); + jmethodID mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), "getPackageName", "()Ljava/lang/String;"); + jstring jstr = (jstring)(*env)->CallObjectMethod(env, context, mid); + if (Android_JNI_ExceptionOccurred(false)) { + LocalReferenceHolder_Cleanup(&refs); + return NULL; + } - // Call user-provided callback - SDL_ClearError(); - mAndroidFileDialogData.callback(mAndroidFileDialogData.userdata, (const char *const *) charFileList, filter); - mAndroidFileDialogData.callback = NULL; + const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL); + char *retval = cstr ? SDL_strdup(cstr) : NULL; + (*env)->ReleaseStringUTFChars(env, jstr, cstr); - // Cleanup memory - for (int i = 0; i < count; i++) { - SDL_free(charFileList[i]); - } - SDL_free(charFileList); - } + LocalReferenceHolder_Cleanup(&refs); + + return retval; } +#ifndef SDL_DIALOG_DISABLED bool Android_JNI_ShowFileDialog( SDL_DialogFileCallback callback, void *userdata, const SDL_DialogFileFilter *filters, int nfilters, SDL_FileDialogType type, @@ -3583,38 +3706,6 @@ bool Android_JNI_ShowFileDialog( return true; } - -SDL_FormFactor SDL_GetAndroidDeviceFormFactor(void) -{ - JNIEnv *env = Android_JNI_GetEnv(); - SDL_FormFactor form_factor = SDL_FORMFACTOR_UNKNOWN; - jstring string; - - string = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetDeviceFormFactor); - if (string) { - const char *utf = (*env)->GetStringUTFChars(env, string, 0); - if (utf) { - if (SDL_strcmp(utf, "tv") == 0) { - form_factor = SDL_FORMFACTOR_TV; - } else if (SDL_strcmp(utf, "tablet") == 0) { - form_factor = SDL_FORMFACTOR_TABLET; - } else if (SDL_strcmp(utf, "phone") == 0) { - form_factor = SDL_FORMFACTOR_PHONE; - } else if (SDL_strcmp(utf, "car") == 0) { - form_factor = SDL_FORMFACTOR_CAR; - } else if (SDL_strcmp(utf, "headset") == 0) { - form_factor = SDL_FORMFACTOR_HEADSET; - } else if (SDL_strcmp(utf, "watch") == 0) { - form_factor = SDL_FORMFACTOR_WATCH; - } else { - form_factor = SDL_FORMFACTOR_UNKNOWN; - } - (*env)->ReleaseStringUTFChars(env, string, utf); - } - (*env)->DeleteLocalRef(env, string); - } - - return form_factor; -} +#endif // !SDL_DIALOG_DISABLED #endif // SDL_PLATFORM_ANDROID diff --git a/src/core/android/SDL_android.h b/src/core/android/SDL_android.h index caa098d32ae33..ebdafa3343dc5 100644 --- a/src/core/android/SDL_android.h +++ b/src/core/android/SDL_android.h @@ -30,17 +30,17 @@ extern "C" { /* *INDENT-ON* */ #endif -#ifndef SDL_VIDEO_DISABLED -#include -#include -#endif - #ifndef SDL_AUDIO_DISABLED +// Audio support #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 -#endif + +void Android_StartAudioHotplug(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording); +void Android_StopAudioHotplug(void); +extern void Android_AudioThreadInit(SDL_AudioDevice *device); +#endif // !SDL_AUDIO_DISABLED // Life cycle typedef enum @@ -62,6 +62,9 @@ void Android_UnlockActivityMutex(void); void Android_SetAllowRecreateActivity(bool enabled); #ifndef SDL_VIDEO_DISABLED +#include +#include + // 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); @@ -71,24 +74,33 @@ extern bool Android_JNI_ShouldMinimizeOnFocusLoss(void); extern void Android_JNI_ShowScreenKeyboard(int input_type, SDL_Rect *inputRect); extern void Android_JNI_HideScreenKeyboard(void); +bool Android_JNI_SuspendScreenSaver(bool suspend); extern ANativeWindow *Android_JNI_GetNativeWindow(void); -#endif // !SDL_VIDEO_DISABLED - -// Kept outside the video guard for the camera driver; stays SDL_ORIENTATION_UNKNOWN when video is disabled (only the video Java layer updates it). extern SDL_DisplayOrientation Android_JNI_GetDisplayNaturalOrientation(void); extern SDL_DisplayOrientation Android_JNI_GetDisplayCurrentOrientation(void); -#ifndef SDL_AUDIO_DISABLED -// Audio support -void Android_StartAudioHotplug(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording); -void Android_StopAudioHotplug(void); -extern void Android_AudioThreadInit(SDL_AudioDevice *device); -#endif // !SDL_AUDIO_DISABLED +// Clipboard support +bool Android_JNI_SetClipboardText(const char *text); +char *Android_JNI_GetClipboardText(void); +bool Android_JNI_HasClipboardText(void); -// Detecting device type -extern bool Android_IsDeXMode(void); -extern bool Android_IsChromebook(void); +// Touch support +void Android_JNI_InitTouch(void); + +// 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); + +// MessageBox +bool Android_JNI_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID); +#endif // !SDL_VIDEO_DISABLED bool Android_JNI_FileOpen(void **puserdata, const char *fileName, const char *mode); Sint64 Android_JNI_FileSize(void *userdata); @@ -103,34 +115,25 @@ bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info); void Android_JNI_GetManifestEnvironmentVariables(void); int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode); -#ifndef SDL_VIDEO_DISABLED -// Clipboard support -bool Android_JNI_SetClipboardText(const char *text); -char *Android_JNI_GetClipboardText(void); -bool Android_JNI_HasClipboardText(void); -#endif // !SDL_VIDEO_DISABLED - +#ifndef SDL_POWER_DISABLED // Power support int Android_JNI_GetPowerInfo(int *plugged, int *charged, int *battery, int *seconds, int *percent); +#endif // !SDL_POWER_DISABLED +#ifndef SDL_JOYSTICK_DISABLED // Joystick support void Android_JNI_DetectDevices(void); void Android_JNI_JoystickSetLED(int device_id, int red, int green, int blue); void Android_JNI_JoystickSetSensorsEnabled(int device_id, bool enabled); +#endif // !SDL_JOYSTICK_DISABLED +#ifndef SDL_HAPTIC_DISABLED // Haptic support void Android_JNI_DetectHapticDevices(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); - -#ifndef SDL_VIDEO_DISABLED -// Touch support -void Android_JNI_InitTouch(void); -#endif // !SDL_VIDEO_DISABLED +#endif // !SDL_HAPTIC_DISABLED // Threads #include @@ -143,21 +146,6 @@ 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); - -#ifndef SDL_VIDEO_DISABLED -// 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); -#endif // !SDL_VIDEO_DISABLED - // Show toast notification bool Android_JNI_ShowToast(const char *message, int duration, int gravity, int xOffset, int yOffset); @@ -171,10 +159,11 @@ SDL_FormFactor SDL_GetAndroidDeviceFormFactor(void); char *SDL_GetAndroidPackageName(void); // this is a SDL_malloc'd string the caller will own. +#ifndef SDL_DIALOG_DISABLED // File Dialogs -bool Android_JNI_ShowFileDialog(SDL_DialogFileCallback callback, void *userdata, - const SDL_DialogFileFilter *filters, int nfilters, SDL_FileDialogType type, - bool multiple, const char *initialPath); +bool Android_JNI_ShowFileDialog(SDL_DialogFileCallback callback, void *userdata, const SDL_DialogFileFilter *filters, + int nfilters, SDL_FileDialogType type, bool multiple, const char *initialPath); +#endif // !SDL_DIALOG_DISABLED // Ends C function definitions when using C++ #ifdef __cplusplus diff --git a/src/joystick/android/SDL_sysjoystick.c b/src/joystick/android/SDL_sysjoystick.c index 710625edbe5ce..2cf2f3e2df17a 100644 --- a/src/joystick/android/SDL_sysjoystick.c +++ b/src/joystick/android/SDL_sysjoystick.c @@ -683,10 +683,14 @@ static bool ANDROID_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_ return SDL_Unsupported(); } +#ifndef SDL_HAPTIC_DISABLED float low_frequency_intensity = (float)low_frequency_rumble / SDL_MAX_UINT16; float high_frequency_intensity = (float)high_frequency_rumble / SDL_MAX_UINT16; Android_JNI_HapticRumble(item->device_id, low_frequency_intensity, high_frequency_intensity, 5000); return true; +#else + return SDL_Unsupported(); +#endif } static bool ANDROID_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)