diff options
| author | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
| commit | 5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch) | |
| tree | 8926ab44f168acf787d8e19608857b3af0f82758 /contrib/SDL-3.2.8/android-project | |
Initial commit
Diffstat (limited to 'contrib/SDL-3.2.8/android-project')
35 files changed, 6199 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/android-project/app/build.gradle b/contrib/SDL-3.2.8/android-project/app/build.gradle new file mode 100644 index 0000000..8946de6 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/build.gradle | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | plugins { | ||
| 2 | id 'com.android.application' | ||
| 3 | } | ||
| 4 | |||
| 5 | def buildWithCMake = project.hasProperty('BUILD_WITH_CMAKE'); | ||
| 6 | |||
| 7 | android { | ||
| 8 | namespace "org.libsdl.app" | ||
| 9 | compileSdkVersion 35 | ||
| 10 | defaultConfig { | ||
| 11 | minSdkVersion 21 | ||
| 12 | targetSdkVersion 35 | ||
| 13 | versionCode 1 | ||
| 14 | versionName "1.0" | ||
| 15 | externalNativeBuild { | ||
| 16 | ndkBuild { | ||
| 17 | arguments "APP_PLATFORM=android-19" | ||
| 18 | // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' | ||
| 19 | abiFilters 'arm64-v8a' | ||
| 20 | } | ||
| 21 | cmake { | ||
| 22 | arguments "-DANDROID_PLATFORM=android-19", "-DANDROID_STL=c++_static" | ||
| 23 | // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' | ||
| 24 | abiFilters 'arm64-v8a' | ||
| 25 | } | ||
| 26 | } | ||
| 27 | } | ||
| 28 | buildTypes { | ||
| 29 | release { | ||
| 30 | minifyEnabled false | ||
| 31 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||
| 32 | } | ||
| 33 | } | ||
| 34 | applicationVariants.all { variant -> | ||
| 35 | tasks["merge${variant.name.capitalize()}Assets"] | ||
| 36 | .dependsOn("externalNativeBuild${variant.name.capitalize()}") | ||
| 37 | } | ||
| 38 | if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) { | ||
| 39 | sourceSets.main { | ||
| 40 | jniLibs.srcDir 'libs' | ||
| 41 | } | ||
| 42 | externalNativeBuild { | ||
| 43 | if (buildWithCMake) { | ||
| 44 | cmake { | ||
| 45 | path 'jni/CMakeLists.txt' | ||
| 46 | } | ||
| 47 | } else { | ||
| 48 | ndkBuild { | ||
| 49 | path 'jni/Android.mk' | ||
| 50 | } | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | } | ||
| 55 | lint { | ||
| 56 | abortOnError false | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | dependencies { | ||
| 61 | implementation fileTree(include: ['*.jar'], dir: 'libs') | ||
| 62 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/jni/Android.mk b/contrib/SDL-3.2.8/android-project/app/jni/Android.mk new file mode 100644 index 0000000..5053e7d --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/jni/Android.mk | |||
| @@ -0,0 +1 @@ | |||
| include $(call all-subdir-makefiles) | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/jni/Application.mk b/contrib/SDL-3.2.8/android-project/app/jni/Application.mk new file mode 100644 index 0000000..023bc20 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/jni/Application.mk | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | |||
| 2 | # Uncomment this if you're using STL in your project | ||
| 3 | # You can find more information here: | ||
| 4 | # https://developer.android.com/ndk/guides/cpp-support | ||
| 5 | # APP_STL := c++_shared | ||
| 6 | |||
| 7 | APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 | ||
| 8 | |||
| 9 | # Min runtime API level | ||
| 10 | APP_PLATFORM=android-16 | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/jni/CMakeLists.txt b/contrib/SDL-3.2.8/android-project/app/jni/CMakeLists.txt new file mode 100644 index 0000000..404b87b --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/jni/CMakeLists.txt | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | cmake_minimum_required(VERSION 3.6) | ||
| 2 | |||
| 3 | project(GAME) | ||
| 4 | |||
| 5 | # SDL sources are in a subfolder named "SDL" | ||
| 6 | add_subdirectory(SDL) | ||
| 7 | |||
| 8 | # Compilation of companion libraries | ||
| 9 | #add_subdirectory(SDL_image) | ||
| 10 | #add_subdirectory(SDL_mixer) | ||
| 11 | #add_subdirectory(SDL_ttf) | ||
| 12 | |||
| 13 | # Your game and its CMakeLists.txt are in a subfolder named "src" | ||
| 14 | add_subdirectory(src) | ||
| 15 | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/jni/src/Android.mk b/contrib/SDL-3.2.8/android-project/app/jni/src/Android.mk new file mode 100644 index 0000000..61672d4 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/jni/src/Android.mk | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | LOCAL_PATH := $(call my-dir) | ||
| 2 | |||
| 3 | include $(CLEAR_VARS) | ||
| 4 | |||
| 5 | LOCAL_MODULE := main | ||
| 6 | |||
| 7 | # Add your application source files here... | ||
| 8 | LOCAL_SRC_FILES := \ | ||
| 9 | YourSourceHere.c | ||
| 10 | |||
| 11 | SDL_PATH := ../SDL # SDL | ||
| 12 | |||
| 13 | LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include # SDL | ||
| 14 | |||
| 15 | LOCAL_SHARED_LIBRARIES := SDL3 | ||
| 16 | |||
| 17 | LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid # SDL | ||
| 18 | |||
| 19 | include $(BUILD_SHARED_LIBRARY) | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/jni/src/CMakeLists.txt b/contrib/SDL-3.2.8/android-project/app/jni/src/CMakeLists.txt new file mode 100644 index 0000000..41a82f2 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/jni/src/CMakeLists.txt | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | cmake_minimum_required(VERSION 3.6) | ||
| 2 | |||
| 3 | project(my_app) | ||
| 4 | |||
| 5 | if(NOT TARGET SDL3::SDL3) | ||
| 6 | find_package(SDL3 CONFIG) | ||
| 7 | endif() | ||
| 8 | |||
| 9 | if(NOT TARGET SDL3::SDL3) | ||
| 10 | find_library(SDL3_LIBRARY NAMES "SDL3") | ||
| 11 | find_path(SDL3_INCLUDE_DIR NAMES "SDL3/SDL.h") | ||
| 12 | add_library(SDL3::SDL3 UNKNOWN IMPORTED) | ||
| 13 | set_property(TARGET SDL3::SDL3 PROPERTY IMPORTED_LOCATION "${SDL3_LIBRARY}") | ||
| 14 | set_property(TARGET SDL3::SDL3 PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${SDL3_INCLUDE_DIR}") | ||
| 15 | endif() | ||
| 16 | |||
| 17 | if(NOT TARGET SDL3::SDL3) | ||
| 18 | message(FATAL_ERROR "Cannot find SDL3. | ||
| 19 | |||
| 20 | Possible ways to fix this: | ||
| 21 | - Use a SDL3 Android aar archive, and configure gradle to use it: prefab is required. | ||
| 22 | - Add add_subdirectory(path/to/SDL) to your CMake script, and make sure a vendored SDL is present there. | ||
| 23 | ") | ||
| 24 | endif() | ||
| 25 | |||
| 26 | add_library(main SHARED | ||
| 27 | YourSourceHere.c | ||
| 28 | ) | ||
| 29 | target_link_libraries(main PRIVATE SDL3::SDL3) | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/jni/src/YourSourceHere.c b/contrib/SDL-3.2.8/android-project/app/jni/src/YourSourceHere.c new file mode 100644 index 0000000..87b8297 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/jni/src/YourSourceHere.c | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | #include <SDL3/SDL.h> | ||
| 2 | #include <SDL3/SDL_main.h> | ||
| 3 | |||
| 4 | /* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ | ||
| 5 | /* */ | ||
| 6 | /* Remove this source, and replace with your SDL sources */ | ||
| 7 | /* */ | ||
| 8 | /* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ | ||
| 9 | |||
| 10 | int main(int argc, char *argv[]) { | ||
| 11 | (void)argc; | ||
| 12 | (void)argv; | ||
| 13 | if (!SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO)) { | ||
| 14 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed (%s)", SDL_GetError()); | ||
| 15 | return 1; | ||
| 16 | } | ||
| 17 | |||
| 18 | if (!SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Hello World", | ||
| 19 | "!! Your SDL project successfully runs on Android !!", NULL)) { | ||
| 20 | SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_ShowSimpleMessageBox failed (%s)", SDL_GetError()); | ||
| 21 | return 1; | ||
| 22 | } | ||
| 23 | |||
| 24 | SDL_Quit(); | ||
| 25 | return 0; | ||
| 26 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/proguard-rules.pro b/contrib/SDL-3.2.8/android-project/app/proguard-rules.pro new file mode 100644 index 0000000..1eeb90e --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/proguard-rules.pro | |||
| @@ -0,0 +1,76 @@ | |||
| 1 | # Add project specific ProGuard rules here. | ||
| 2 | # By default, the flags in this file are appended to flags specified | ||
| 3 | # in [sdk]/tools/proguard/proguard-android.txt | ||
| 4 | # You can edit the include path and order by changing the proguardFiles | ||
| 5 | # directive in build.gradle. | ||
| 6 | # | ||
| 7 | # For more details, see | ||
| 8 | # https://developer.android.com/build/shrink-code | ||
| 9 | |||
| 10 | # Add any project specific keep options here: | ||
| 11 | |||
| 12 | # If your project uses WebView with JS, uncomment the following | ||
| 13 | # and specify the fully qualified class name to the JavaScript interface | ||
| 14 | # class: | ||
| 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
| 16 | # public *; | ||
| 17 | #} | ||
| 18 | |||
| 19 | -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity { | ||
| 20 | java.lang.String nativeGetHint(java.lang.String); # Java-side doesn't use this, so it gets minified, but C-side still tries to register it | ||
| 21 | java.lang.String clipboardGetText(); | ||
| 22 | boolean clipboardHasText(); | ||
| 23 | void clipboardSetText(java.lang.String); | ||
| 24 | int createCustomCursor(int[], int, int, int, int); | ||
| 25 | void destroyCustomCursor(int); | ||
| 26 | android.content.Context getContext(); | ||
| 27 | boolean getManifestEnvironmentVariables(); | ||
| 28 | android.view.Surface getNativeSurface(); | ||
| 29 | void initTouch(); | ||
| 30 | boolean isAndroidTV(); | ||
| 31 | boolean isChromebook(); | ||
| 32 | boolean isDeXMode(); | ||
| 33 | boolean isScreenKeyboardShown(); | ||
| 34 | boolean isTablet(); | ||
| 35 | void manualBackButton(); | ||
| 36 | int messageboxShowMessageBox(int, java.lang.String, java.lang.String, int[], int[], java.lang.String[], int[]); | ||
| 37 | void minimizeWindow(); | ||
| 38 | boolean openURL(java.lang.String); | ||
| 39 | void onNativePen(int, int, int , float , float , float); | ||
| 40 | void requestPermission(java.lang.String, int); | ||
| 41 | boolean showToast(java.lang.String, int, int, int, int); | ||
| 42 | boolean sendMessage(int, int); | ||
| 43 | boolean setActivityTitle(java.lang.String); | ||
| 44 | boolean setCustomCursor(int); | ||
| 45 | void setOrientation(int, int, boolean, java.lang.String); | ||
| 46 | boolean setRelativeMouseEnabled(boolean); | ||
| 47 | boolean setSystemCursor(int); | ||
| 48 | void setWindowStyle(boolean); | ||
| 49 | boolean shouldMinimizeOnFocusLoss(); | ||
| 50 | boolean showTextInput(int, int, int, int, int); | ||
| 51 | boolean supportsRelativeMouse(); | ||
| 52 | int openFileDescriptor(java.lang.String, java.lang.String); | ||
| 53 | boolean showFileDialog(java.lang.String[], boolean, boolean, int); | ||
| 54 | } | ||
| 55 | |||
| 56 | -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.HIDDeviceManager { | ||
| 57 | void closeDevice(int); | ||
| 58 | boolean initialize(boolean, boolean); | ||
| 59 | boolean openDevice(int); | ||
| 60 | boolean readReport(int, byte[], boolean); | ||
| 61 | int writeReport(int, byte[], boolean); | ||
| 62 | } | ||
| 63 | |||
| 64 | -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLAudioManager { | ||
| 65 | void registerAudioDeviceCallback(); | ||
| 66 | void unregisterAudioDeviceCallback(); | ||
| 67 | void audioSetThreadPriority(boolean, int); | ||
| 68 | } | ||
| 69 | |||
| 70 | -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLControllerManager { | ||
| 71 | void pollInputDevices(); | ||
| 72 | void pollHapticDevices(); | ||
| 73 | void hapticRun(int, float, int); | ||
| 74 | void hapticRumble(int, float, float, int); | ||
| 75 | void hapticStop(int); | ||
| 76 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/AndroidManifest.xml b/contrib/SDL-3.2.8/android-project/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f3a7cd5 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/AndroidManifest.xml | |||
| @@ -0,0 +1,107 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | android:versionCode="1" | ||
| 4 | android:versionName="1.0" | ||
| 5 | android:installLocation="auto"> | ||
| 6 | |||
| 7 | <!-- OpenGL ES 2.0 --> | ||
| 8 | <uses-feature android:glEsVersion="0x00020000" /> | ||
| 9 | |||
| 10 | <!-- Touchscreen support --> | ||
| 11 | <uses-feature | ||
| 12 | android:name="android.hardware.touchscreen" | ||
| 13 | android:required="false" /> | ||
| 14 | |||
| 15 | <!-- Game controller support --> | ||
| 16 | <uses-feature | ||
| 17 | android:name="android.hardware.bluetooth" | ||
| 18 | android:required="false" /> | ||
| 19 | <uses-feature | ||
| 20 | android:name="android.hardware.gamepad" | ||
| 21 | android:required="false" /> | ||
| 22 | <uses-feature | ||
| 23 | android:name="android.hardware.usb.host" | ||
| 24 | android:required="false" /> | ||
| 25 | |||
| 26 | <!-- External mouse input events --> | ||
| 27 | <uses-feature | ||
| 28 | android:name="android.hardware.type.pc" | ||
| 29 | android:required="false" /> | ||
| 30 | |||
| 31 | <!-- Audio recording support --> | ||
| 32 | <!-- if you want to record audio, uncomment this. --> | ||
| 33 | <!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> --> | ||
| 34 | <!-- <uses-feature | ||
| 35 | android:name="android.hardware.microphone" | ||
| 36 | android:required="false" /> --> | ||
| 37 | |||
| 38 | <!-- Camera support --> | ||
| 39 | <!-- if you want to record video, uncomment this. --> | ||
| 40 | <!-- | ||
| 41 | <uses-permission android:name="android.permission.CAMERA" /> | ||
| 42 | <uses-feature android:name="android.hardware.camera" /> | ||
| 43 | --> | ||
| 44 | |||
| 45 | <!-- Allow downloading to the external storage on Android 5.1 and older --> | ||
| 46 | <!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> --> | ||
| 47 | |||
| 48 | <!-- Allow access to Bluetooth devices --> | ||
| 49 | <!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM --> | ||
| 50 | <!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> --> | ||
| 51 | <!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> --> | ||
| 52 | |||
| 53 | <!-- Allow access to the vibrator --> | ||
| 54 | <uses-permission android:name="android.permission.VIBRATE" /> | ||
| 55 | |||
| 56 | <!-- Allow access to Internet --> | ||
| 57 | <!-- if you want to connect to the network or internet, uncomment this. --> | ||
| 58 | <!-- | ||
| 59 | <uses-permission android:name="android.permission.INTERNET" /> | ||
| 60 | --> | ||
| 61 | |||
| 62 | <!-- Create a Java class extending SDLActivity and place it in a | ||
| 63 | directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java | ||
| 64 | |||
| 65 | then replace "SDLActivity" with the name of your class (e.g. "MyGame") | ||
| 66 | in the XML below. | ||
| 67 | |||
| 68 | An example Java class can be found in README-android.md | ||
| 69 | --> | ||
| 70 | <application android:label="@string/app_name" | ||
| 71 | android:icon="@mipmap/ic_launcher" | ||
| 72 | android:allowBackup="true" | ||
| 73 | android:theme="@style/AppTheme" | ||
| 74 | android:hardwareAccelerated="true" > | ||
| 75 | |||
| 76 | <!-- Example of setting SDL hints from AndroidManifest.xml: | ||
| 77 | <meta-data android:name="SDL_ENV.SDL_ANDROID_TRAP_BACK_BUTTON" android:value="0"/> | ||
| 78 | --> | ||
| 79 | |||
| 80 | <activity android:name="SDLActivity" | ||
| 81 | android:label="@string/app_name" | ||
| 82 | android:alwaysRetainTaskState="true" | ||
| 83 | android:launchMode="singleInstance" | ||
| 84 | android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation" | ||
| 85 | android:preferMinimalPostProcessing="true" | ||
| 86 | android:exported="true" | ||
| 87 | > | ||
| 88 | <intent-filter> | ||
| 89 | <action android:name="android.intent.action.MAIN" /> | ||
| 90 | <category android:name="android.intent.category.LAUNCHER" /> | ||
| 91 | </intent-filter> | ||
| 92 | <!-- Let Android know that we can handle some USB devices and should receive this event --> | ||
| 93 | <intent-filter> | ||
| 94 | <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> | ||
| 95 | </intent-filter> | ||
| 96 | <!-- Drop file event --> | ||
| 97 | <!-- | ||
| 98 | <intent-filter> | ||
| 99 | <action android:name="android.intent.action.VIEW" /> | ||
| 100 | <category android:name="android.intent.category.DEFAULT" /> | ||
| 101 | <data android:mimeType="*/*" /> | ||
| 102 | </intent-filter> | ||
| 103 | --> | ||
| 104 | </activity> | ||
| 105 | </application> | ||
| 106 | |||
| 107 | </manifest> | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java new file mode 100644 index 0000000..f960953 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java | |||
| @@ -0,0 +1,21 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.hardware.usb.UsbDevice; | ||
| 4 | |||
| 5 | interface HIDDevice | ||
| 6 | { | ||
| 7 | public int getId(); | ||
| 8 | public int getVendorId(); | ||
| 9 | public int getProductId(); | ||
| 10 | public String getSerialNumber(); | ||
| 11 | public int getVersion(); | ||
| 12 | public String getManufacturerName(); | ||
| 13 | public String getProductName(); | ||
| 14 | public UsbDevice getDevice(); | ||
| 15 | public boolean open(); | ||
| 16 | public int writeReport(byte[] report, boolean feature); | ||
| 17 | public boolean readReport(byte[] report, boolean feature); | ||
| 18 | public void setFrozen(boolean frozen); | ||
| 19 | public void close(); | ||
| 20 | public void shutdown(); | ||
| 21 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java new file mode 100644 index 0000000..d2dc0d2 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java | |||
| @@ -0,0 +1,645 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.content.Context; | ||
| 4 | import android.bluetooth.BluetoothDevice; | ||
| 5 | import android.bluetooth.BluetoothGatt; | ||
| 6 | import android.bluetooth.BluetoothGattCallback; | ||
| 7 | import android.bluetooth.BluetoothGattCharacteristic; | ||
| 8 | import android.bluetooth.BluetoothGattDescriptor; | ||
| 9 | import android.bluetooth.BluetoothManager; | ||
| 10 | import android.bluetooth.BluetoothProfile; | ||
| 11 | import android.bluetooth.BluetoothGattService; | ||
| 12 | import android.hardware.usb.UsbDevice; | ||
| 13 | import android.os.Handler; | ||
| 14 | import android.os.Looper; | ||
| 15 | import android.util.Log; | ||
| 16 | import android.os.*; | ||
| 17 | |||
| 18 | //import com.android.internal.util.HexDump; | ||
| 19 | |||
| 20 | import java.lang.Runnable; | ||
| 21 | import java.util.Arrays; | ||
| 22 | import java.util.LinkedList; | ||
| 23 | import java.util.UUID; | ||
| 24 | |||
| 25 | class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { | ||
| 26 | |||
| 27 | private static final String TAG = "hidapi"; | ||
| 28 | private HIDDeviceManager mManager; | ||
| 29 | private BluetoothDevice mDevice; | ||
| 30 | private int mDeviceId; | ||
| 31 | private BluetoothGatt mGatt; | ||
| 32 | private boolean mIsRegistered = false; | ||
| 33 | private boolean mIsConnected = false; | ||
| 34 | private boolean mIsChromebook = false; | ||
| 35 | private boolean mIsReconnecting = false; | ||
| 36 | private boolean mFrozen = false; | ||
| 37 | private LinkedList<GattOperation> mOperations; | ||
| 38 | GattOperation mCurrentOperation = null; | ||
| 39 | private Handler mHandler; | ||
| 40 | |||
| 41 | private static final int TRANSPORT_AUTO = 0; | ||
| 42 | private static final int TRANSPORT_BREDR = 1; | ||
| 43 | private static final int TRANSPORT_LE = 2; | ||
| 44 | |||
| 45 | private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; | ||
| 46 | |||
| 47 | static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); | ||
| 48 | static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); | ||
| 49 | static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); | ||
| 50 | static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; | ||
| 51 | |||
| 52 | static class GattOperation { | ||
| 53 | private enum Operation { | ||
| 54 | CHR_READ, | ||
| 55 | CHR_WRITE, | ||
| 56 | ENABLE_NOTIFICATION | ||
| 57 | } | ||
| 58 | |||
| 59 | Operation mOp; | ||
| 60 | UUID mUuid; | ||
| 61 | byte[] mValue; | ||
| 62 | BluetoothGatt mGatt; | ||
| 63 | boolean mResult = true; | ||
| 64 | |||
| 65 | private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { | ||
| 66 | mGatt = gatt; | ||
| 67 | mOp = operation; | ||
| 68 | mUuid = uuid; | ||
| 69 | } | ||
| 70 | |||
| 71 | private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { | ||
| 72 | mGatt = gatt; | ||
| 73 | mOp = operation; | ||
| 74 | mUuid = uuid; | ||
| 75 | mValue = value; | ||
| 76 | } | ||
| 77 | |||
| 78 | public void run() { | ||
| 79 | // This is executed in main thread | ||
| 80 | BluetoothGattCharacteristic chr; | ||
| 81 | |||
| 82 | switch (mOp) { | ||
| 83 | case CHR_READ: | ||
| 84 | chr = getCharacteristic(mUuid); | ||
| 85 | //Log.v(TAG, "Reading characteristic " + chr.getUuid()); | ||
| 86 | if (!mGatt.readCharacteristic(chr)) { | ||
| 87 | Log.e(TAG, "Unable to read characteristic " + mUuid.toString()); | ||
| 88 | mResult = false; | ||
| 89 | break; | ||
| 90 | } | ||
| 91 | mResult = true; | ||
| 92 | break; | ||
| 93 | case CHR_WRITE: | ||
| 94 | chr = getCharacteristic(mUuid); | ||
| 95 | //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value)); | ||
| 96 | chr.setValue(mValue); | ||
| 97 | if (!mGatt.writeCharacteristic(chr)) { | ||
| 98 | Log.e(TAG, "Unable to write characteristic " + mUuid.toString()); | ||
| 99 | mResult = false; | ||
| 100 | break; | ||
| 101 | } | ||
| 102 | mResult = true; | ||
| 103 | break; | ||
| 104 | case ENABLE_NOTIFICATION: | ||
| 105 | chr = getCharacteristic(mUuid); | ||
| 106 | //Log.v(TAG, "Writing descriptor of " + chr.getUuid()); | ||
| 107 | if (chr != null) { | ||
| 108 | BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); | ||
| 109 | if (cccd != null) { | ||
| 110 | int properties = chr.getProperties(); | ||
| 111 | byte[] value; | ||
| 112 | if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) { | ||
| 113 | value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; | ||
| 114 | } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) { | ||
| 115 | value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; | ||
| 116 | } else { | ||
| 117 | Log.e(TAG, "Unable to start notifications on input characteristic"); | ||
| 118 | mResult = false; | ||
| 119 | return; | ||
| 120 | } | ||
| 121 | |||
| 122 | mGatt.setCharacteristicNotification(chr, true); | ||
| 123 | cccd.setValue(value); | ||
| 124 | if (!mGatt.writeDescriptor(cccd)) { | ||
| 125 | Log.e(TAG, "Unable to write descriptor " + mUuid.toString()); | ||
| 126 | mResult = false; | ||
| 127 | return; | ||
| 128 | } | ||
| 129 | mResult = true; | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | public boolean finish() { | ||
| 136 | return mResult; | ||
| 137 | } | ||
| 138 | |||
| 139 | private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { | ||
| 140 | BluetoothGattService valveService = mGatt.getService(steamControllerService); | ||
| 141 | if (valveService == null) | ||
| 142 | return null; | ||
| 143 | return valveService.getCharacteristic(uuid); | ||
| 144 | } | ||
| 145 | |||
| 146 | static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) { | ||
| 147 | return new GattOperation(gatt, Operation.CHR_READ, uuid); | ||
| 148 | } | ||
| 149 | |||
| 150 | static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) { | ||
| 151 | return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value); | ||
| 152 | } | ||
| 153 | |||
| 154 | static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { | ||
| 155 | return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { | ||
| 160 | mManager = manager; | ||
| 161 | mDevice = device; | ||
| 162 | mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); | ||
| 163 | mIsRegistered = false; | ||
| 164 | mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); | ||
| 165 | mOperations = new LinkedList<GattOperation>(); | ||
| 166 | mHandler = new Handler(Looper.getMainLooper()); | ||
| 167 | |||
| 168 | mGatt = connectGatt(); | ||
| 169 | // final HIDDeviceBLESteamController finalThis = this; | ||
| 170 | // mHandler.postDelayed(new Runnable() { | ||
| 171 | // @Override | ||
| 172 | // public void run() { | ||
| 173 | // finalThis.checkConnectionForChromebookIssue(); | ||
| 174 | // } | ||
| 175 | // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); | ||
| 176 | } | ||
| 177 | |||
| 178 | public String getIdentifier() { | ||
| 179 | return String.format("SteamController.%s", mDevice.getAddress()); | ||
| 180 | } | ||
| 181 | |||
| 182 | public BluetoothGatt getGatt() { | ||
| 183 | return mGatt; | ||
| 184 | } | ||
| 185 | |||
| 186 | // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead | ||
| 187 | // of TRANSPORT_LE. Let's force ourselves to connect low energy. | ||
| 188 | private BluetoothGatt connectGatt(boolean managed) { | ||
| 189 | if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) { | ||
| 190 | try { | ||
| 191 | return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE); | ||
| 192 | } catch (Exception e) { | ||
| 193 | return mDevice.connectGatt(mManager.getContext(), managed, this); | ||
| 194 | } | ||
| 195 | } else { | ||
| 196 | return mDevice.connectGatt(mManager.getContext(), managed, this); | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | private BluetoothGatt connectGatt() { | ||
| 201 | return connectGatt(false); | ||
| 202 | } | ||
| 203 | |||
| 204 | protected int getConnectionState() { | ||
| 205 | |||
| 206 | Context context = mManager.getContext(); | ||
| 207 | if (context == null) { | ||
| 208 | // We are lacking any context to get our Bluetooth information. We'll just assume disconnected. | ||
| 209 | return BluetoothProfile.STATE_DISCONNECTED; | ||
| 210 | } | ||
| 211 | |||
| 212 | BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE); | ||
| 213 | if (btManager == null) { | ||
| 214 | // This device doesn't support Bluetooth. We should never be here, because how did | ||
| 215 | // we instantiate a device to start with? | ||
| 216 | return BluetoothProfile.STATE_DISCONNECTED; | ||
| 217 | } | ||
| 218 | |||
| 219 | return btManager.getConnectionState(mDevice, BluetoothProfile.GATT); | ||
| 220 | } | ||
| 221 | |||
| 222 | public void reconnect() { | ||
| 223 | |||
| 224 | if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) { | ||
| 225 | mGatt.disconnect(); | ||
| 226 | mGatt = connectGatt(); | ||
| 227 | } | ||
| 228 | |||
| 229 | } | ||
| 230 | |||
| 231 | protected void checkConnectionForChromebookIssue() { | ||
| 232 | if (!mIsChromebook) { | ||
| 233 | // We only do this on Chromebooks, because otherwise it's really annoying to just attempt | ||
| 234 | // over and over. | ||
| 235 | return; | ||
| 236 | } | ||
| 237 | |||
| 238 | int connectionState = getConnectionState(); | ||
| 239 | |||
| 240 | switch (connectionState) { | ||
| 241 | case BluetoothProfile.STATE_CONNECTED: | ||
| 242 | if (!mIsConnected) { | ||
| 243 | // We are in the Bad Chromebook Place. We can force a disconnect | ||
| 244 | // to try to recover. | ||
| 245 | Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect."); | ||
| 246 | mIsReconnecting = true; | ||
| 247 | mGatt.disconnect(); | ||
| 248 | mGatt = connectGatt(false); | ||
| 249 | break; | ||
| 250 | } | ||
| 251 | else if (!isRegistered()) { | ||
| 252 | if (mGatt.getServices().size() > 0) { | ||
| 253 | Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover."); | ||
| 254 | probeService(this); | ||
| 255 | } | ||
| 256 | else { | ||
| 257 | Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover."); | ||
| 258 | mIsReconnecting = true; | ||
| 259 | mGatt.disconnect(); | ||
| 260 | mGatt = connectGatt(false); | ||
| 261 | break; | ||
| 262 | } | ||
| 263 | } | ||
| 264 | else { | ||
| 265 | Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!"); | ||
| 266 | return; | ||
| 267 | } | ||
| 268 | break; | ||
| 269 | |||
| 270 | case BluetoothProfile.STATE_DISCONNECTED: | ||
| 271 | Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover."); | ||
| 272 | |||
| 273 | mIsReconnecting = true; | ||
| 274 | mGatt.disconnect(); | ||
| 275 | mGatt = connectGatt(false); | ||
| 276 | break; | ||
| 277 | |||
| 278 | case BluetoothProfile.STATE_CONNECTING: | ||
| 279 | Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer."); | ||
| 280 | break; | ||
| 281 | } | ||
| 282 | |||
| 283 | final HIDDeviceBLESteamController finalThis = this; | ||
| 284 | mHandler.postDelayed(new Runnable() { | ||
| 285 | @Override | ||
| 286 | public void run() { | ||
| 287 | finalThis.checkConnectionForChromebookIssue(); | ||
| 288 | } | ||
| 289 | }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); | ||
| 290 | } | ||
| 291 | |||
| 292 | private boolean isRegistered() { | ||
| 293 | return mIsRegistered; | ||
| 294 | } | ||
| 295 | |||
| 296 | private void setRegistered() { | ||
| 297 | mIsRegistered = true; | ||
| 298 | } | ||
| 299 | |||
| 300 | private boolean probeService(HIDDeviceBLESteamController controller) { | ||
| 301 | |||
| 302 | if (isRegistered()) { | ||
| 303 | return true; | ||
| 304 | } | ||
| 305 | |||
| 306 | if (!mIsConnected) { | ||
| 307 | return false; | ||
| 308 | } | ||
| 309 | |||
| 310 | Log.v(TAG, "probeService controller=" + controller); | ||
| 311 | |||
| 312 | for (BluetoothGattService service : mGatt.getServices()) { | ||
| 313 | if (service.getUuid().equals(steamControllerService)) { | ||
| 314 | Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); | ||
| 315 | |||
| 316 | for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { | ||
| 317 | if (chr.getUuid().equals(inputCharacteristic)) { | ||
| 318 | Log.v(TAG, "Found input characteristic"); | ||
| 319 | // Start notifications | ||
| 320 | BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); | ||
| 321 | if (cccd != null) { | ||
| 322 | enableNotification(chr.getUuid()); | ||
| 323 | } | ||
| 324 | } | ||
| 325 | } | ||
| 326 | return true; | ||
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) { | ||
| 331 | Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us."); | ||
| 332 | mIsConnected = false; | ||
| 333 | mIsReconnecting = true; | ||
| 334 | mGatt.disconnect(); | ||
| 335 | mGatt = connectGatt(false); | ||
| 336 | } | ||
| 337 | |||
| 338 | return false; | ||
| 339 | } | ||
| 340 | |||
| 341 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 342 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 343 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 344 | |||
| 345 | private void finishCurrentGattOperation() { | ||
| 346 | GattOperation op = null; | ||
| 347 | synchronized (mOperations) { | ||
| 348 | if (mCurrentOperation != null) { | ||
| 349 | op = mCurrentOperation; | ||
| 350 | mCurrentOperation = null; | ||
| 351 | } | ||
| 352 | } | ||
| 353 | if (op != null) { | ||
| 354 | boolean result = op.finish(); // TODO: Maybe in main thread as well? | ||
| 355 | |||
| 356 | // Our operation failed, let's add it back to the beginning of our queue. | ||
| 357 | if (!result) { | ||
| 358 | mOperations.addFirst(op); | ||
| 359 | } | ||
| 360 | } | ||
| 361 | executeNextGattOperation(); | ||
| 362 | } | ||
| 363 | |||
| 364 | private void executeNextGattOperation() { | ||
| 365 | synchronized (mOperations) { | ||
| 366 | if (mCurrentOperation != null) | ||
| 367 | return; | ||
| 368 | |||
| 369 | if (mOperations.isEmpty()) | ||
| 370 | return; | ||
| 371 | |||
| 372 | mCurrentOperation = mOperations.removeFirst(); | ||
| 373 | } | ||
| 374 | |||
| 375 | // Run in main thread | ||
| 376 | mHandler.post(new Runnable() { | ||
| 377 | @Override | ||
| 378 | public void run() { | ||
| 379 | synchronized (mOperations) { | ||
| 380 | if (mCurrentOperation == null) { | ||
| 381 | Log.e(TAG, "Current operation null in executor?"); | ||
| 382 | return; | ||
| 383 | } | ||
| 384 | |||
| 385 | mCurrentOperation.run(); | ||
| 386 | // now wait for the GATT callback and when it comes, finish this operation | ||
| 387 | } | ||
| 388 | } | ||
| 389 | }); | ||
| 390 | } | ||
| 391 | |||
| 392 | private void queueGattOperation(GattOperation op) { | ||
| 393 | synchronized (mOperations) { | ||
| 394 | mOperations.add(op); | ||
| 395 | } | ||
| 396 | executeNextGattOperation(); | ||
| 397 | } | ||
| 398 | |||
| 399 | private void enableNotification(UUID chrUuid) { | ||
| 400 | GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); | ||
| 401 | queueGattOperation(op); | ||
| 402 | } | ||
| 403 | |||
| 404 | public void writeCharacteristic(UUID uuid, byte[] value) { | ||
| 405 | GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); | ||
| 406 | queueGattOperation(op); | ||
| 407 | } | ||
| 408 | |||
| 409 | public void readCharacteristic(UUID uuid) { | ||
| 410 | GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); | ||
| 411 | queueGattOperation(op); | ||
| 412 | } | ||
| 413 | |||
| 414 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 415 | ////////////// BluetoothGattCallback overridden methods | ||
| 416 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 417 | |||
| 418 | public void onConnectionStateChange(BluetoothGatt g, int status, int newState) { | ||
| 419 | //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState); | ||
| 420 | mIsReconnecting = false; | ||
| 421 | if (newState == 2) { | ||
| 422 | mIsConnected = true; | ||
| 423 | // Run directly, without GattOperation | ||
| 424 | if (!isRegistered()) { | ||
| 425 | mHandler.post(new Runnable() { | ||
| 426 | @Override | ||
| 427 | public void run() { | ||
| 428 | mGatt.discoverServices(); | ||
| 429 | } | ||
| 430 | }); | ||
| 431 | } | ||
| 432 | } | ||
| 433 | else if (newState == 0) { | ||
| 434 | mIsConnected = false; | ||
| 435 | } | ||
| 436 | |||
| 437 | // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent. | ||
| 438 | } | ||
| 439 | |||
| 440 | public void onServicesDiscovered(BluetoothGatt gatt, int status) { | ||
| 441 | //Log.v(TAG, "onServicesDiscovered status=" + status); | ||
| 442 | if (status == 0) { | ||
| 443 | if (gatt.getServices().size() == 0) { | ||
| 444 | Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack."); | ||
| 445 | mIsReconnecting = true; | ||
| 446 | mIsConnected = false; | ||
| 447 | gatt.disconnect(); | ||
| 448 | mGatt = connectGatt(false); | ||
| 449 | } | ||
| 450 | else { | ||
| 451 | probeService(this); | ||
| 452 | } | ||
| 453 | } | ||
| 454 | } | ||
| 455 | |||
| 456 | public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { | ||
| 457 | //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid()); | ||
| 458 | |||
| 459 | if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { | ||
| 460 | mManager.HIDDeviceReportResponse(getId(), characteristic.getValue()); | ||
| 461 | } | ||
| 462 | |||
| 463 | finishCurrentGattOperation(); | ||
| 464 | } | ||
| 465 | |||
| 466 | public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { | ||
| 467 | //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid()); | ||
| 468 | |||
| 469 | if (characteristic.getUuid().equals(reportCharacteristic)) { | ||
| 470 | // Only register controller with the native side once it has been fully configured | ||
| 471 | if (!isRegistered()) { | ||
| 472 | Log.v(TAG, "Registering Steam Controller with ID: " + getId()); | ||
| 473 | mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true); | ||
| 474 | setRegistered(); | ||
| 475 | } | ||
| 476 | } | ||
| 477 | |||
| 478 | finishCurrentGattOperation(); | ||
| 479 | } | ||
| 480 | |||
| 481 | public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { | ||
| 482 | // Enable this for verbose logging of controller input reports | ||
| 483 | //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); | ||
| 484 | |||
| 485 | if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { | ||
| 486 | mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); | ||
| 487 | } | ||
| 488 | } | ||
| 489 | |||
| 490 | public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { | ||
| 491 | //Log.v(TAG, "onDescriptorRead status=" + status); | ||
| 492 | } | ||
| 493 | |||
| 494 | public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { | ||
| 495 | BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); | ||
| 496 | //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); | ||
| 497 | |||
| 498 | if (chr.getUuid().equals(inputCharacteristic)) { | ||
| 499 | boolean hasWrittenInputDescriptor = true; | ||
| 500 | BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); | ||
| 501 | if (reportChr != null) { | ||
| 502 | Log.v(TAG, "Writing report characteristic to enter valve mode"); | ||
| 503 | reportChr.setValue(enterValveMode); | ||
| 504 | gatt.writeCharacteristic(reportChr); | ||
| 505 | } | ||
| 506 | } | ||
| 507 | |||
| 508 | finishCurrentGattOperation(); | ||
| 509 | } | ||
| 510 | |||
| 511 | public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { | ||
| 512 | //Log.v(TAG, "onReliableWriteCompleted status=" + status); | ||
| 513 | } | ||
| 514 | |||
| 515 | public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { | ||
| 516 | //Log.v(TAG, "onReadRemoteRssi status=" + status); | ||
| 517 | } | ||
| 518 | |||
| 519 | public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { | ||
| 520 | //Log.v(TAG, "onMtuChanged status=" + status); | ||
| 521 | } | ||
| 522 | |||
| 523 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 524 | //////// Public API | ||
| 525 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 526 | |||
| 527 | @Override | ||
| 528 | public int getId() { | ||
| 529 | return mDeviceId; | ||
| 530 | } | ||
| 531 | |||
| 532 | @Override | ||
| 533 | public int getVendorId() { | ||
| 534 | // Valve Corporation | ||
| 535 | final int VALVE_USB_VID = 0x28DE; | ||
| 536 | return VALVE_USB_VID; | ||
| 537 | } | ||
| 538 | |||
| 539 | @Override | ||
| 540 | public int getProductId() { | ||
| 541 | // We don't have an easy way to query from the Bluetooth device, but we know what it is | ||
| 542 | final int D0G_BLE2_PID = 0x1106; | ||
| 543 | return D0G_BLE2_PID; | ||
| 544 | } | ||
| 545 | |||
| 546 | @Override | ||
| 547 | public String getSerialNumber() { | ||
| 548 | // This will be read later via feature report by Steam | ||
| 549 | return "12345"; | ||
| 550 | } | ||
| 551 | |||
| 552 | @Override | ||
| 553 | public int getVersion() { | ||
| 554 | return 0; | ||
| 555 | } | ||
| 556 | |||
| 557 | @Override | ||
| 558 | public String getManufacturerName() { | ||
| 559 | return "Valve Corporation"; | ||
| 560 | } | ||
| 561 | |||
| 562 | @Override | ||
| 563 | public String getProductName() { | ||
| 564 | return "Steam Controller"; | ||
| 565 | } | ||
| 566 | |||
| 567 | @Override | ||
| 568 | public UsbDevice getDevice() { | ||
| 569 | return null; | ||
| 570 | } | ||
| 571 | |||
| 572 | @Override | ||
| 573 | public boolean open() { | ||
| 574 | return true; | ||
| 575 | } | ||
| 576 | |||
| 577 | @Override | ||
| 578 | public int writeReport(byte[] report, boolean feature) { | ||
| 579 | if (!isRegistered()) { | ||
| 580 | Log.e(TAG, "Attempted writeReport before Steam Controller is registered!"); | ||
| 581 | if (mIsConnected) { | ||
| 582 | probeService(this); | ||
| 583 | } | ||
| 584 | return -1; | ||
| 585 | } | ||
| 586 | |||
| 587 | if (feature) { | ||
| 588 | // We need to skip the first byte, as that doesn't go over the air | ||
| 589 | byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); | ||
| 590 | //Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report)); | ||
| 591 | writeCharacteristic(reportCharacteristic, actual_report); | ||
| 592 | return report.length; | ||
| 593 | } else { | ||
| 594 | //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); | ||
| 595 | writeCharacteristic(reportCharacteristic, report); | ||
| 596 | return report.length; | ||
| 597 | } | ||
| 598 | } | ||
| 599 | |||
| 600 | @Override | ||
| 601 | public boolean readReport(byte[] report, boolean feature) { | ||
| 602 | if (!isRegistered()) { | ||
| 603 | Log.e(TAG, "Attempted readReport before Steam Controller is registered!"); | ||
| 604 | if (mIsConnected) { | ||
| 605 | probeService(this); | ||
| 606 | } | ||
| 607 | return false; | ||
| 608 | } | ||
| 609 | |||
| 610 | if (feature) { | ||
| 611 | readCharacteristic(reportCharacteristic); | ||
| 612 | return true; | ||
| 613 | } else { | ||
| 614 | // Not implemented | ||
| 615 | return false; | ||
| 616 | } | ||
| 617 | } | ||
| 618 | |||
| 619 | @Override | ||
| 620 | public void close() { | ||
| 621 | } | ||
| 622 | |||
| 623 | @Override | ||
| 624 | public void setFrozen(boolean frozen) { | ||
| 625 | mFrozen = frozen; | ||
| 626 | } | ||
| 627 | |||
| 628 | @Override | ||
| 629 | public void shutdown() { | ||
| 630 | close(); | ||
| 631 | |||
| 632 | BluetoothGatt g = mGatt; | ||
| 633 | if (g != null) { | ||
| 634 | g.disconnect(); | ||
| 635 | g.close(); | ||
| 636 | mGatt = null; | ||
| 637 | } | ||
| 638 | mManager = null; | ||
| 639 | mIsRegistered = false; | ||
| 640 | mIsConnected = false; | ||
| 641 | mOperations.clear(); | ||
| 642 | } | ||
| 643 | |||
| 644 | } | ||
| 645 | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java new file mode 100644 index 0000000..37d80ca --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java | |||
| @@ -0,0 +1,689 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.app.Activity; | ||
| 4 | import android.app.AlertDialog; | ||
| 5 | import android.app.PendingIntent; | ||
| 6 | import android.bluetooth.BluetoothAdapter; | ||
| 7 | import android.bluetooth.BluetoothDevice; | ||
| 8 | import android.bluetooth.BluetoothManager; | ||
| 9 | import android.bluetooth.BluetoothProfile; | ||
| 10 | import android.os.Build; | ||
| 11 | import android.util.Log; | ||
| 12 | import android.content.BroadcastReceiver; | ||
| 13 | import android.content.Context; | ||
| 14 | import android.content.DialogInterface; | ||
| 15 | import android.content.Intent; | ||
| 16 | import android.content.IntentFilter; | ||
| 17 | import android.content.SharedPreferences; | ||
| 18 | import android.content.pm.PackageManager; | ||
| 19 | import android.hardware.usb.*; | ||
| 20 | import android.os.Handler; | ||
| 21 | import android.os.Looper; | ||
| 22 | |||
| 23 | import java.util.ArrayList; | ||
| 24 | import java.util.HashMap; | ||
| 25 | import java.util.Iterator; | ||
| 26 | import java.util.List; | ||
| 27 | |||
| 28 | public class HIDDeviceManager { | ||
| 29 | private static final String TAG = "hidapi"; | ||
| 30 | private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION"; | ||
| 31 | |||
| 32 | private static HIDDeviceManager sManager; | ||
| 33 | private static int sManagerRefCount = 0; | ||
| 34 | |||
| 35 | public static HIDDeviceManager acquire(Context context) { | ||
| 36 | if (sManagerRefCount == 0) { | ||
| 37 | sManager = new HIDDeviceManager(context); | ||
| 38 | } | ||
| 39 | ++sManagerRefCount; | ||
| 40 | return sManager; | ||
| 41 | } | ||
| 42 | |||
| 43 | public static void release(HIDDeviceManager manager) { | ||
| 44 | if (manager == sManager) { | ||
| 45 | --sManagerRefCount; | ||
| 46 | if (sManagerRefCount == 0) { | ||
| 47 | sManager.close(); | ||
| 48 | sManager = null; | ||
| 49 | } | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | private Context mContext; | ||
| 54 | private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>(); | ||
| 55 | private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>(); | ||
| 56 | private int mNextDeviceId = 0; | ||
| 57 | private SharedPreferences mSharedPreferences = null; | ||
| 58 | private boolean mIsChromebook = false; | ||
| 59 | private UsbManager mUsbManager; | ||
| 60 | private Handler mHandler; | ||
| 61 | private BluetoothManager mBluetoothManager; | ||
| 62 | private List<BluetoothDevice> mLastBluetoothDevices; | ||
| 63 | |||
| 64 | private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() { | ||
| 65 | @Override | ||
| 66 | public void onReceive(Context context, Intent intent) { | ||
| 67 | String action = intent.getAction(); | ||
| 68 | if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { | ||
| 69 | UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); | ||
| 70 | handleUsbDeviceAttached(usbDevice); | ||
| 71 | } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { | ||
| 72 | UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); | ||
| 73 | handleUsbDeviceDetached(usbDevice); | ||
| 74 | } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) { | ||
| 75 | UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); | ||
| 76 | handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | }; | ||
| 80 | |||
| 81 | private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() { | ||
| 82 | @Override | ||
| 83 | public void onReceive(Context context, Intent intent) { | ||
| 84 | String action = intent.getAction(); | ||
| 85 | // Bluetooth device was connected. If it was a Steam Controller, handle it | ||
| 86 | if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { | ||
| 87 | BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); | ||
| 88 | Log.d(TAG, "Bluetooth device connected: " + device); | ||
| 89 | |||
| 90 | if (isSteamController(device)) { | ||
| 91 | connectBluetoothDevice(device); | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | // Bluetooth device was disconnected, remove from controller manager (if any) | ||
| 96 | if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { | ||
| 97 | BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); | ||
| 98 | Log.d(TAG, "Bluetooth device disconnected: " + device); | ||
| 99 | |||
| 100 | disconnectBluetoothDevice(device); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | }; | ||
| 104 | |||
| 105 | private HIDDeviceManager(final Context context) { | ||
| 106 | mContext = context; | ||
| 107 | |||
| 108 | HIDDeviceRegisterCallback(); | ||
| 109 | |||
| 110 | mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); | ||
| 111 | mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); | ||
| 112 | |||
| 113 | // if (shouldClear) { | ||
| 114 | // SharedPreferences.Editor spedit = mSharedPreferences.edit(); | ||
| 115 | // spedit.clear(); | ||
| 116 | // spedit.commit(); | ||
| 117 | // } | ||
| 118 | // else | ||
| 119 | { | ||
| 120 | mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); | ||
| 121 | } | ||
| 122 | } | ||
| 123 | |||
| 124 | public Context getContext() { | ||
| 125 | return mContext; | ||
| 126 | } | ||
| 127 | |||
| 128 | public int getDeviceIDForIdentifier(String identifier) { | ||
| 129 | SharedPreferences.Editor spedit = mSharedPreferences.edit(); | ||
| 130 | |||
| 131 | int result = mSharedPreferences.getInt(identifier, 0); | ||
| 132 | if (result == 0) { | ||
| 133 | result = mNextDeviceId++; | ||
| 134 | spedit.putInt("next_device_id", mNextDeviceId); | ||
| 135 | } | ||
| 136 | |||
| 137 | spedit.putInt(identifier, result); | ||
| 138 | spedit.commit(); | ||
| 139 | return result; | ||
| 140 | } | ||
| 141 | |||
| 142 | private void initializeUSB() { | ||
| 143 | mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); | ||
| 144 | if (mUsbManager == null) { | ||
| 145 | return; | ||
| 146 | } | ||
| 147 | |||
| 148 | /* | ||
| 149 | // Logging | ||
| 150 | for (UsbDevice device : mUsbManager.getDeviceList().values()) { | ||
| 151 | Log.i(TAG,"Path: " + device.getDeviceName()); | ||
| 152 | Log.i(TAG,"Manufacturer: " + device.getManufacturerName()); | ||
| 153 | Log.i(TAG,"Product: " + device.getProductName()); | ||
| 154 | Log.i(TAG,"ID: " + device.getDeviceId()); | ||
| 155 | Log.i(TAG,"Class: " + device.getDeviceClass()); | ||
| 156 | Log.i(TAG,"Protocol: " + device.getDeviceProtocol()); | ||
| 157 | Log.i(TAG,"Vendor ID " + device.getVendorId()); | ||
| 158 | Log.i(TAG,"Product ID: " + device.getProductId()); | ||
| 159 | Log.i(TAG,"Interface count: " + device.getInterfaceCount()); | ||
| 160 | Log.i(TAG,"---------------------------------------"); | ||
| 161 | |||
| 162 | // Get interface details | ||
| 163 | for (int index = 0; index < device.getInterfaceCount(); index++) { | ||
| 164 | UsbInterface mUsbInterface = device.getInterface(index); | ||
| 165 | Log.i(TAG," ***** *****"); | ||
| 166 | Log.i(TAG," Interface index: " + index); | ||
| 167 | Log.i(TAG," Interface ID: " + mUsbInterface.getId()); | ||
| 168 | Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass()); | ||
| 169 | Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass()); | ||
| 170 | Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); | ||
| 171 | Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); | ||
| 172 | |||
| 173 | // Get endpoint details | ||
| 174 | for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) | ||
| 175 | { | ||
| 176 | UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); | ||
| 177 | Log.i(TAG," ++++ ++++ ++++"); | ||
| 178 | Log.i(TAG," Endpoint index: " + epi); | ||
| 179 | Log.i(TAG," Attributes: " + mEndpoint.getAttributes()); | ||
| 180 | Log.i(TAG," Direction: " + mEndpoint.getDirection()); | ||
| 181 | Log.i(TAG," Number: " + mEndpoint.getEndpointNumber()); | ||
| 182 | Log.i(TAG," Interval: " + mEndpoint.getInterval()); | ||
| 183 | Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize()); | ||
| 184 | Log.i(TAG," Type: " + mEndpoint.getType()); | ||
| 185 | } | ||
| 186 | } | ||
| 187 | } | ||
| 188 | Log.i(TAG," No more devices connected."); | ||
| 189 | */ | ||
| 190 | |||
| 191 | // Register for USB broadcasts and permission completions | ||
| 192 | IntentFilter filter = new IntentFilter(); | ||
| 193 | filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); | ||
| 194 | filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); | ||
| 195 | filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); | ||
| 196 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||
| 197 | mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED); | ||
| 198 | } else { | ||
| 199 | mContext.registerReceiver(mUsbBroadcast, filter); | ||
| 200 | } | ||
| 201 | |||
| 202 | for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { | ||
| 203 | handleUsbDeviceAttached(usbDevice); | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | UsbManager getUSBManager() { | ||
| 208 | return mUsbManager; | ||
| 209 | } | ||
| 210 | |||
| 211 | private void shutdownUSB() { | ||
| 212 | try { | ||
| 213 | mContext.unregisterReceiver(mUsbBroadcast); | ||
| 214 | } catch (Exception e) { | ||
| 215 | // We may not have registered, that's okay | ||
| 216 | } | ||
| 217 | } | ||
| 218 | |||
| 219 | private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) { | ||
| 220 | if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { | ||
| 221 | return true; | ||
| 222 | } | ||
| 223 | if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) { | ||
| 224 | return true; | ||
| 225 | } | ||
| 226 | return false; | ||
| 227 | } | ||
| 228 | |||
| 229 | private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) { | ||
| 230 | final int XB360_IFACE_SUBCLASS = 93; | ||
| 231 | final int XB360_IFACE_PROTOCOL = 1; // Wired | ||
| 232 | final int XB360W_IFACE_PROTOCOL = 129; // Wireless | ||
| 233 | final int[] SUPPORTED_VENDORS = { | ||
| 234 | 0x0079, // GPD Win 2 | ||
| 235 | 0x044f, // Thrustmaster | ||
| 236 | 0x045e, // Microsoft | ||
| 237 | 0x046d, // Logitech | ||
| 238 | 0x056e, // Elecom | ||
| 239 | 0x06a3, // Saitek | ||
| 240 | 0x0738, // Mad Catz | ||
| 241 | 0x07ff, // Mad Catz | ||
| 242 | 0x0e6f, // PDP | ||
| 243 | 0x0f0d, // Hori | ||
| 244 | 0x1038, // SteelSeries | ||
| 245 | 0x11c9, // Nacon | ||
| 246 | 0x12ab, // Unknown | ||
| 247 | 0x1430, // RedOctane | ||
| 248 | 0x146b, // BigBen | ||
| 249 | 0x1532, // Razer Sabertooth | ||
| 250 | 0x15e4, // Numark | ||
| 251 | 0x162e, // Joytech | ||
| 252 | 0x1689, // Razer Onza | ||
| 253 | 0x1949, // Lab126, Inc. | ||
| 254 | 0x1bad, // Harmonix | ||
| 255 | 0x20d6, // PowerA | ||
| 256 | 0x24c6, // PowerA | ||
| 257 | 0x2c22, // Qanba | ||
| 258 | 0x2dc8, // 8BitDo | ||
| 259 | 0x9886, // ASTRO Gaming | ||
| 260 | }; | ||
| 261 | |||
| 262 | if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && | ||
| 263 | usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS && | ||
| 264 | (usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL || | ||
| 265 | usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) { | ||
| 266 | int vendor_id = usbDevice.getVendorId(); | ||
| 267 | for (int supportedVid : SUPPORTED_VENDORS) { | ||
| 268 | if (vendor_id == supportedVid) { | ||
| 269 | return true; | ||
| 270 | } | ||
| 271 | } | ||
| 272 | } | ||
| 273 | return false; | ||
| 274 | } | ||
| 275 | |||
| 276 | private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) { | ||
| 277 | final int XB1_IFACE_SUBCLASS = 71; | ||
| 278 | final int XB1_IFACE_PROTOCOL = 208; | ||
| 279 | final int[] SUPPORTED_VENDORS = { | ||
| 280 | 0x03f0, // HP | ||
| 281 | 0x044f, // Thrustmaster | ||
| 282 | 0x045e, // Microsoft | ||
| 283 | 0x0738, // Mad Catz | ||
| 284 | 0x0b05, // ASUS | ||
| 285 | 0x0e6f, // PDP | ||
| 286 | 0x0f0d, // Hori | ||
| 287 | 0x10f5, // Turtle Beach | ||
| 288 | 0x1532, // Razer Wildcat | ||
| 289 | 0x20d6, // PowerA | ||
| 290 | 0x24c6, // PowerA | ||
| 291 | 0x2dc8, // 8BitDo | ||
| 292 | 0x2e24, // Hyperkin | ||
| 293 | 0x3537, // GameSir | ||
| 294 | }; | ||
| 295 | |||
| 296 | if (usbInterface.getId() == 0 && | ||
| 297 | usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && | ||
| 298 | usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS && | ||
| 299 | usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) { | ||
| 300 | int vendor_id = usbDevice.getVendorId(); | ||
| 301 | for (int supportedVid : SUPPORTED_VENDORS) { | ||
| 302 | if (vendor_id == supportedVid) { | ||
| 303 | return true; | ||
| 304 | } | ||
| 305 | } | ||
| 306 | } | ||
| 307 | return false; | ||
| 308 | } | ||
| 309 | |||
| 310 | private void handleUsbDeviceAttached(UsbDevice usbDevice) { | ||
| 311 | connectHIDDeviceUSB(usbDevice); | ||
| 312 | } | ||
| 313 | |||
| 314 | private void handleUsbDeviceDetached(UsbDevice usbDevice) { | ||
| 315 | List<Integer> devices = new ArrayList<Integer>(); | ||
| 316 | for (HIDDevice device : mDevicesById.values()) { | ||
| 317 | if (usbDevice.equals(device.getDevice())) { | ||
| 318 | devices.add(device.getId()); | ||
| 319 | } | ||
| 320 | } | ||
| 321 | for (int id : devices) { | ||
| 322 | HIDDevice device = mDevicesById.get(id); | ||
| 323 | mDevicesById.remove(id); | ||
| 324 | device.shutdown(); | ||
| 325 | HIDDeviceDisconnected(id); | ||
| 326 | } | ||
| 327 | } | ||
| 328 | |||
| 329 | private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) { | ||
| 330 | for (HIDDevice device : mDevicesById.values()) { | ||
| 331 | if (usbDevice.equals(device.getDevice())) { | ||
| 332 | boolean opened = false; | ||
| 333 | if (permission_granted) { | ||
| 334 | opened = device.open(); | ||
| 335 | } | ||
| 336 | HIDDeviceOpenResult(device.getId(), opened); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | } | ||
| 340 | |||
| 341 | private void connectHIDDeviceUSB(UsbDevice usbDevice) { | ||
| 342 | synchronized (this) { | ||
| 343 | int interface_mask = 0; | ||
| 344 | for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) { | ||
| 345 | UsbInterface usbInterface = usbDevice.getInterface(interface_index); | ||
| 346 | if (isHIDDeviceInterface(usbDevice, usbInterface)) { | ||
| 347 | // Check to see if we've already added this interface | ||
| 348 | // This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive | ||
| 349 | int interface_id = usbInterface.getId(); | ||
| 350 | if ((interface_mask & (1 << interface_id)) != 0) { | ||
| 351 | continue; | ||
| 352 | } | ||
| 353 | interface_mask |= (1 << interface_id); | ||
| 354 | |||
| 355 | HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); | ||
| 356 | int id = device.getId(); | ||
| 357 | mDevicesById.put(id, device); | ||
| 358 | HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false); | ||
| 359 | } | ||
| 360 | } | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | private void initializeBluetooth() { | ||
| 365 | Log.d(TAG, "Initializing Bluetooth"); | ||
| 366 | |||
| 367 | if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ && | ||
| 368 | mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { | ||
| 369 | Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT"); | ||
| 370 | return; | ||
| 371 | } | ||
| 372 | |||
| 373 | if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ && | ||
| 374 | mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { | ||
| 375 | Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); | ||
| 376 | return; | ||
| 377 | } | ||
| 378 | |||
| 379 | if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) { | ||
| 380 | Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE"); | ||
| 381 | return; | ||
| 382 | } | ||
| 383 | |||
| 384 | // Find bonded bluetooth controllers and create SteamControllers for them | ||
| 385 | mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE); | ||
| 386 | if (mBluetoothManager == null) { | ||
| 387 | // This device doesn't support Bluetooth. | ||
| 388 | return; | ||
| 389 | } | ||
| 390 | |||
| 391 | BluetoothAdapter btAdapter = mBluetoothManager.getAdapter(); | ||
| 392 | if (btAdapter == null) { | ||
| 393 | // This device has Bluetooth support in the codebase, but has no available adapters. | ||
| 394 | return; | ||
| 395 | } | ||
| 396 | |||
| 397 | // Get our bonded devices. | ||
| 398 | for (BluetoothDevice device : btAdapter.getBondedDevices()) { | ||
| 399 | |||
| 400 | Log.d(TAG, "Bluetooth device available: " + device); | ||
| 401 | if (isSteamController(device)) { | ||
| 402 | connectBluetoothDevice(device); | ||
| 403 | } | ||
| 404 | |||
| 405 | } | ||
| 406 | |||
| 407 | // NOTE: These don't work on Chromebooks, to my undying dismay. | ||
| 408 | IntentFilter filter = new IntentFilter(); | ||
| 409 | filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); | ||
| 410 | filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); | ||
| 411 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||
| 412 | mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED); | ||
| 413 | } else { | ||
| 414 | mContext.registerReceiver(mBluetoothBroadcast, filter); | ||
| 415 | } | ||
| 416 | |||
| 417 | if (mIsChromebook) { | ||
| 418 | mHandler = new Handler(Looper.getMainLooper()); | ||
| 419 | mLastBluetoothDevices = new ArrayList<BluetoothDevice>(); | ||
| 420 | |||
| 421 | // final HIDDeviceManager finalThis = this; | ||
| 422 | // mHandler.postDelayed(new Runnable() { | ||
| 423 | // @Override | ||
| 424 | // public void run() { | ||
| 425 | // finalThis.chromebookConnectionHandler(); | ||
| 426 | // } | ||
| 427 | // }, 5000); | ||
| 428 | } | ||
| 429 | } | ||
| 430 | |||
| 431 | private void shutdownBluetooth() { | ||
| 432 | try { | ||
| 433 | mContext.unregisterReceiver(mBluetoothBroadcast); | ||
| 434 | } catch (Exception e) { | ||
| 435 | // We may not have registered, that's okay | ||
| 436 | } | ||
| 437 | } | ||
| 438 | |||
| 439 | // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly. | ||
| 440 | // This function provides a sort of dummy version of that, watching for changes in the | ||
| 441 | // connected devices and attempting to add controllers as things change. | ||
| 442 | public void chromebookConnectionHandler() { | ||
| 443 | if (!mIsChromebook) { | ||
| 444 | return; | ||
| 445 | } | ||
| 446 | |||
| 447 | ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>(); | ||
| 448 | ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>(); | ||
| 449 | |||
| 450 | List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); | ||
| 451 | |||
| 452 | for (BluetoothDevice bluetoothDevice : currentConnected) { | ||
| 453 | if (!mLastBluetoothDevices.contains(bluetoothDevice)) { | ||
| 454 | connected.add(bluetoothDevice); | ||
| 455 | } | ||
| 456 | } | ||
| 457 | for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) { | ||
| 458 | if (!currentConnected.contains(bluetoothDevice)) { | ||
| 459 | disconnected.add(bluetoothDevice); | ||
| 460 | } | ||
| 461 | } | ||
| 462 | |||
| 463 | mLastBluetoothDevices = currentConnected; | ||
| 464 | |||
| 465 | for (BluetoothDevice bluetoothDevice : disconnected) { | ||
| 466 | disconnectBluetoothDevice(bluetoothDevice); | ||
| 467 | } | ||
| 468 | for (BluetoothDevice bluetoothDevice : connected) { | ||
| 469 | connectBluetoothDevice(bluetoothDevice); | ||
| 470 | } | ||
| 471 | |||
| 472 | final HIDDeviceManager finalThis = this; | ||
| 473 | mHandler.postDelayed(new Runnable() { | ||
| 474 | @Override | ||
| 475 | public void run() { | ||
| 476 | finalThis.chromebookConnectionHandler(); | ||
| 477 | } | ||
| 478 | }, 10000); | ||
| 479 | } | ||
| 480 | |||
| 481 | public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { | ||
| 482 | Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice); | ||
| 483 | synchronized (this) { | ||
| 484 | if (mBluetoothDevices.containsKey(bluetoothDevice)) { | ||
| 485 | Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect"); | ||
| 486 | |||
| 487 | HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); | ||
| 488 | device.reconnect(); | ||
| 489 | |||
| 490 | return false; | ||
| 491 | } | ||
| 492 | HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice); | ||
| 493 | int id = device.getId(); | ||
| 494 | mBluetoothDevices.put(bluetoothDevice, device); | ||
| 495 | mDevicesById.put(id, device); | ||
| 496 | |||
| 497 | // The Steam Controller will mark itself connected once initialization is complete | ||
| 498 | } | ||
| 499 | return true; | ||
| 500 | } | ||
| 501 | |||
| 502 | public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { | ||
| 503 | synchronized (this) { | ||
| 504 | HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); | ||
| 505 | if (device == null) | ||
| 506 | return; | ||
| 507 | |||
| 508 | int id = device.getId(); | ||
| 509 | mBluetoothDevices.remove(bluetoothDevice); | ||
| 510 | mDevicesById.remove(id); | ||
| 511 | device.shutdown(); | ||
| 512 | HIDDeviceDisconnected(id); | ||
| 513 | } | ||
| 514 | } | ||
| 515 | |||
| 516 | public boolean isSteamController(BluetoothDevice bluetoothDevice) { | ||
| 517 | // Sanity check. If you pass in a null device, by definition it is never a Steam Controller. | ||
| 518 | if (bluetoothDevice == null) { | ||
| 519 | return false; | ||
| 520 | } | ||
| 521 | |||
| 522 | // If the device has no local name, we really don't want to try an equality check against it. | ||
| 523 | if (bluetoothDevice.getName() == null) { | ||
| 524 | return false; | ||
| 525 | } | ||
| 526 | |||
| 527 | return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); | ||
| 528 | } | ||
| 529 | |||
| 530 | private void close() { | ||
| 531 | shutdownUSB(); | ||
| 532 | shutdownBluetooth(); | ||
| 533 | synchronized (this) { | ||
| 534 | for (HIDDevice device : mDevicesById.values()) { | ||
| 535 | device.shutdown(); | ||
| 536 | } | ||
| 537 | mDevicesById.clear(); | ||
| 538 | mBluetoothDevices.clear(); | ||
| 539 | HIDDeviceReleaseCallback(); | ||
| 540 | } | ||
| 541 | } | ||
| 542 | |||
| 543 | public void setFrozen(boolean frozen) { | ||
| 544 | synchronized (this) { | ||
| 545 | for (HIDDevice device : mDevicesById.values()) { | ||
| 546 | device.setFrozen(frozen); | ||
| 547 | } | ||
| 548 | } | ||
| 549 | } | ||
| 550 | |||
| 551 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 552 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 553 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 554 | |||
| 555 | private HIDDevice getDevice(int id) { | ||
| 556 | synchronized (this) { | ||
| 557 | HIDDevice result = mDevicesById.get(id); | ||
| 558 | if (result == null) { | ||
| 559 | Log.v(TAG, "No device for id: " + id); | ||
| 560 | Log.v(TAG, "Available devices: " + mDevicesById.keySet()); | ||
| 561 | } | ||
| 562 | return result; | ||
| 563 | } | ||
| 564 | } | ||
| 565 | |||
| 566 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 567 | ////////// JNI interface functions | ||
| 568 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 569 | |||
| 570 | public boolean initialize(boolean usb, boolean bluetooth) { | ||
| 571 | Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); | ||
| 572 | |||
| 573 | if (usb) { | ||
| 574 | initializeUSB(); | ||
| 575 | } | ||
| 576 | if (bluetooth) { | ||
| 577 | initializeBluetooth(); | ||
| 578 | } | ||
| 579 | return true; | ||
| 580 | } | ||
| 581 | |||
| 582 | public boolean openDevice(int deviceID) { | ||
| 583 | Log.v(TAG, "openDevice deviceID=" + deviceID); | ||
| 584 | HIDDevice device = getDevice(deviceID); | ||
| 585 | if (device == null) { | ||
| 586 | HIDDeviceDisconnected(deviceID); | ||
| 587 | return false; | ||
| 588 | } | ||
| 589 | |||
| 590 | // Look to see if this is a USB device and we have permission to access it | ||
| 591 | UsbDevice usbDevice = device.getDevice(); | ||
| 592 | if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) { | ||
| 593 | HIDDeviceOpenPending(deviceID); | ||
| 594 | try { | ||
| 595 | final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31 | ||
| 596 | int flags; | ||
| 597 | if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { | ||
| 598 | flags = FLAG_MUTABLE; | ||
| 599 | } else { | ||
| 600 | flags = 0; | ||
| 601 | } | ||
| 602 | if (Build.VERSION.SDK_INT >= 33 /* Android 14.0 (U) */) { | ||
| 603 | Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION); | ||
| 604 | intent.setPackage(mContext.getPackageName()); | ||
| 605 | mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags)); | ||
| 606 | } else { | ||
| 607 | mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags)); | ||
| 608 | } | ||
| 609 | } catch (Exception e) { | ||
| 610 | Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); | ||
| 611 | HIDDeviceOpenResult(deviceID, false); | ||
| 612 | } | ||
| 613 | return false; | ||
| 614 | } | ||
| 615 | |||
| 616 | try { | ||
| 617 | return device.open(); | ||
| 618 | } catch (Exception e) { | ||
| 619 | Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); | ||
| 620 | } | ||
| 621 | return false; | ||
| 622 | } | ||
| 623 | |||
| 624 | public int writeReport(int deviceID, byte[] report, boolean feature) { | ||
| 625 | try { | ||
| 626 | //Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length); | ||
| 627 | HIDDevice device; | ||
| 628 | device = getDevice(deviceID); | ||
| 629 | if (device == null) { | ||
| 630 | HIDDeviceDisconnected(deviceID); | ||
| 631 | return -1; | ||
| 632 | } | ||
| 633 | |||
| 634 | return device.writeReport(report, feature); | ||
| 635 | } catch (Exception e) { | ||
| 636 | Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); | ||
| 637 | } | ||
| 638 | return -1; | ||
| 639 | } | ||
| 640 | |||
| 641 | public boolean readReport(int deviceID, byte[] report, boolean feature) { | ||
| 642 | try { | ||
| 643 | //Log.v(TAG, "readReport deviceID=" + deviceID); | ||
| 644 | HIDDevice device; | ||
| 645 | device = getDevice(deviceID); | ||
| 646 | if (device == null) { | ||
| 647 | HIDDeviceDisconnected(deviceID); | ||
| 648 | return false; | ||
| 649 | } | ||
| 650 | |||
| 651 | return device.readReport(report, feature); | ||
| 652 | } catch (Exception e) { | ||
| 653 | Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); | ||
| 654 | } | ||
| 655 | return false; | ||
| 656 | } | ||
| 657 | |||
| 658 | public void closeDevice(int deviceID) { | ||
| 659 | try { | ||
| 660 | Log.v(TAG, "closeDevice deviceID=" + deviceID); | ||
| 661 | HIDDevice device; | ||
| 662 | device = getDevice(deviceID); | ||
| 663 | if (device == null) { | ||
| 664 | HIDDeviceDisconnected(deviceID); | ||
| 665 | return; | ||
| 666 | } | ||
| 667 | |||
| 668 | device.close(); | ||
| 669 | } catch (Exception e) { | ||
| 670 | Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); | ||
| 671 | } | ||
| 672 | } | ||
| 673 | |||
| 674 | |||
| 675 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 676 | /////////////// Native methods | ||
| 677 | ////////////////////////////////////////////////////////////////////////////////////////////////////// | ||
| 678 | |||
| 679 | private native void HIDDeviceRegisterCallback(); | ||
| 680 | private native void HIDDeviceReleaseCallback(); | ||
| 681 | |||
| 682 | native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth); | ||
| 683 | native void HIDDeviceOpenPending(int deviceID); | ||
| 684 | native void HIDDeviceOpenResult(int deviceID, boolean opened); | ||
| 685 | native void HIDDeviceDisconnected(int deviceID); | ||
| 686 | |||
| 687 | native void HIDDeviceInputReport(int deviceID, byte[] report); | ||
| 688 | native void HIDDeviceReportResponse(int deviceID, byte[] report); | ||
| 689 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java new file mode 100644 index 0000000..2741438 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java | |||
| @@ -0,0 +1,318 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.hardware.usb.*; | ||
| 4 | import android.os.Build; | ||
| 5 | import android.util.Log; | ||
| 6 | import java.util.Arrays; | ||
| 7 | |||
| 8 | class HIDDeviceUSB implements HIDDevice { | ||
| 9 | |||
| 10 | private static final String TAG = "hidapi"; | ||
| 11 | |||
| 12 | protected HIDDeviceManager mManager; | ||
| 13 | protected UsbDevice mDevice; | ||
| 14 | protected int mInterfaceIndex; | ||
| 15 | protected int mInterface; | ||
| 16 | protected int mDeviceId; | ||
| 17 | protected UsbDeviceConnection mConnection; | ||
| 18 | protected UsbEndpoint mInputEndpoint; | ||
| 19 | protected UsbEndpoint mOutputEndpoint; | ||
| 20 | protected InputThread mInputThread; | ||
| 21 | protected boolean mRunning; | ||
| 22 | protected boolean mFrozen; | ||
| 23 | |||
| 24 | public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { | ||
| 25 | mManager = manager; | ||
| 26 | mDevice = usbDevice; | ||
| 27 | mInterfaceIndex = interface_index; | ||
| 28 | mInterface = mDevice.getInterface(mInterfaceIndex).getId(); | ||
| 29 | mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); | ||
| 30 | mRunning = false; | ||
| 31 | } | ||
| 32 | |||
| 33 | public String getIdentifier() { | ||
| 34 | return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex); | ||
| 35 | } | ||
| 36 | |||
| 37 | @Override | ||
| 38 | public int getId() { | ||
| 39 | return mDeviceId; | ||
| 40 | } | ||
| 41 | |||
| 42 | @Override | ||
| 43 | public int getVendorId() { | ||
| 44 | return mDevice.getVendorId(); | ||
| 45 | } | ||
| 46 | |||
| 47 | @Override | ||
| 48 | public int getProductId() { | ||
| 49 | return mDevice.getProductId(); | ||
| 50 | } | ||
| 51 | |||
| 52 | @Override | ||
| 53 | public String getSerialNumber() { | ||
| 54 | String result = null; | ||
| 55 | if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { | ||
| 56 | try { | ||
| 57 | result = mDevice.getSerialNumber(); | ||
| 58 | } | ||
| 59 | catch (SecurityException exception) { | ||
| 60 | //Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage()); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | if (result == null) { | ||
| 64 | result = ""; | ||
| 65 | } | ||
| 66 | return result; | ||
| 67 | } | ||
| 68 | |||
| 69 | @Override | ||
| 70 | public int getVersion() { | ||
| 71 | return 0; | ||
| 72 | } | ||
| 73 | |||
| 74 | @Override | ||
| 75 | public String getManufacturerName() { | ||
| 76 | String result = null; | ||
| 77 | if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { | ||
| 78 | result = mDevice.getManufacturerName(); | ||
| 79 | } | ||
| 80 | if (result == null) { | ||
| 81 | result = String.format("%x", getVendorId()); | ||
| 82 | } | ||
| 83 | return result; | ||
| 84 | } | ||
| 85 | |||
| 86 | @Override | ||
| 87 | public String getProductName() { | ||
| 88 | String result = null; | ||
| 89 | if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { | ||
| 90 | result = mDevice.getProductName(); | ||
| 91 | } | ||
| 92 | if (result == null) { | ||
| 93 | result = String.format("%x", getProductId()); | ||
| 94 | } | ||
| 95 | return result; | ||
| 96 | } | ||
| 97 | |||
| 98 | @Override | ||
| 99 | public UsbDevice getDevice() { | ||
| 100 | return mDevice; | ||
| 101 | } | ||
| 102 | |||
| 103 | public String getDeviceName() { | ||
| 104 | return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; | ||
| 105 | } | ||
| 106 | |||
| 107 | @Override | ||
| 108 | public boolean open() { | ||
| 109 | mConnection = mManager.getUSBManager().openDevice(mDevice); | ||
| 110 | if (mConnection == null) { | ||
| 111 | Log.w(TAG, "Unable to open USB device " + getDeviceName()); | ||
| 112 | return false; | ||
| 113 | } | ||
| 114 | |||
| 115 | // Force claim our interface | ||
| 116 | UsbInterface iface = mDevice.getInterface(mInterfaceIndex); | ||
| 117 | if (!mConnection.claimInterface(iface, true)) { | ||
| 118 | Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName()); | ||
| 119 | close(); | ||
| 120 | return false; | ||
| 121 | } | ||
| 122 | |||
| 123 | // Find the endpoints | ||
| 124 | for (int j = 0; j < iface.getEndpointCount(); j++) { | ||
| 125 | UsbEndpoint endpt = iface.getEndpoint(j); | ||
| 126 | switch (endpt.getDirection()) { | ||
| 127 | case UsbConstants.USB_DIR_IN: | ||
| 128 | if (mInputEndpoint == null) { | ||
| 129 | mInputEndpoint = endpt; | ||
| 130 | } | ||
| 131 | break; | ||
| 132 | case UsbConstants.USB_DIR_OUT: | ||
| 133 | if (mOutputEndpoint == null) { | ||
| 134 | mOutputEndpoint = endpt; | ||
| 135 | } | ||
| 136 | break; | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | // Make sure the required endpoints were present | ||
| 141 | if (mInputEndpoint == null || mOutputEndpoint == null) { | ||
| 142 | Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); | ||
| 143 | close(); | ||
| 144 | return false; | ||
| 145 | } | ||
| 146 | |||
| 147 | // Start listening for input | ||
| 148 | mRunning = true; | ||
| 149 | mInputThread = new InputThread(); | ||
| 150 | mInputThread.start(); | ||
| 151 | |||
| 152 | return true; | ||
| 153 | } | ||
| 154 | |||
| 155 | @Override | ||
| 156 | public int writeReport(byte[] report, boolean feature) { | ||
| 157 | if (mConnection == null) { | ||
| 158 | Log.w(TAG, "writeReport() called with no device connection"); | ||
| 159 | return -1; | ||
| 160 | } | ||
| 161 | |||
| 162 | if (feature) { | ||
| 163 | int res = -1; | ||
| 164 | int offset = 0; | ||
| 165 | int length = report.length; | ||
| 166 | boolean skipped_report_id = false; | ||
| 167 | byte report_number = report[0]; | ||
| 168 | |||
| 169 | if (report_number == 0x0) { | ||
| 170 | ++offset; | ||
| 171 | --length; | ||
| 172 | skipped_report_id = true; | ||
| 173 | } | ||
| 174 | |||
| 175 | res = mConnection.controlTransfer( | ||
| 176 | UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, | ||
| 177 | 0x09/*HID set_report*/, | ||
| 178 | (3/*HID feature*/ << 8) | report_number, | ||
| 179 | mInterface, | ||
| 180 | report, offset, length, | ||
| 181 | 1000/*timeout millis*/); | ||
| 182 | |||
| 183 | if (res < 0) { | ||
| 184 | Log.w(TAG, "writeFeatureReport() returned " + res + " on device " + getDeviceName()); | ||
| 185 | return -1; | ||
| 186 | } | ||
| 187 | |||
| 188 | if (skipped_report_id) { | ||
| 189 | ++length; | ||
| 190 | } | ||
| 191 | return length; | ||
| 192 | } else { | ||
| 193 | int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); | ||
| 194 | if (res != report.length) { | ||
| 195 | Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName()); | ||
| 196 | } | ||
| 197 | return res; | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | @Override | ||
| 202 | public boolean readReport(byte[] report, boolean feature) { | ||
| 203 | int res = -1; | ||
| 204 | int offset = 0; | ||
| 205 | int length = report.length; | ||
| 206 | boolean skipped_report_id = false; | ||
| 207 | byte report_number = report[0]; | ||
| 208 | |||
| 209 | if (mConnection == null) { | ||
| 210 | Log.w(TAG, "readReport() called with no device connection"); | ||
| 211 | return false; | ||
| 212 | } | ||
| 213 | |||
| 214 | if (report_number == 0x0) { | ||
| 215 | /* Offset the return buffer by 1, so that the report ID | ||
| 216 | will remain in byte 0. */ | ||
| 217 | ++offset; | ||
| 218 | --length; | ||
| 219 | skipped_report_id = true; | ||
| 220 | } | ||
| 221 | |||
| 222 | res = mConnection.controlTransfer( | ||
| 223 | UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, | ||
| 224 | 0x01/*HID get_report*/, | ||
| 225 | ((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number, | ||
| 226 | mInterface, | ||
| 227 | report, offset, length, | ||
| 228 | 1000/*timeout millis*/); | ||
| 229 | |||
| 230 | if (res < 0) { | ||
| 231 | Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName()); | ||
| 232 | return false; | ||
| 233 | } | ||
| 234 | |||
| 235 | if (skipped_report_id) { | ||
| 236 | ++res; | ||
| 237 | ++length; | ||
| 238 | } | ||
| 239 | |||
| 240 | byte[] data; | ||
| 241 | if (res == length) { | ||
| 242 | data = report; | ||
| 243 | } else { | ||
| 244 | data = Arrays.copyOfRange(report, 0, res); | ||
| 245 | } | ||
| 246 | mManager.HIDDeviceReportResponse(mDeviceId, data); | ||
| 247 | |||
| 248 | return true; | ||
| 249 | } | ||
| 250 | |||
| 251 | @Override | ||
| 252 | public void close() { | ||
| 253 | mRunning = false; | ||
| 254 | if (mInputThread != null) { | ||
| 255 | while (mInputThread.isAlive()) { | ||
| 256 | mInputThread.interrupt(); | ||
| 257 | try { | ||
| 258 | mInputThread.join(); | ||
| 259 | } catch (InterruptedException e) { | ||
| 260 | // Keep trying until we're done | ||
| 261 | } | ||
| 262 | } | ||
| 263 | mInputThread = null; | ||
| 264 | } | ||
| 265 | if (mConnection != null) { | ||
| 266 | UsbInterface iface = mDevice.getInterface(mInterfaceIndex); | ||
| 267 | mConnection.releaseInterface(iface); | ||
| 268 | mConnection.close(); | ||
| 269 | mConnection = null; | ||
| 270 | } | ||
| 271 | } | ||
| 272 | |||
| 273 | @Override | ||
| 274 | public void shutdown() { | ||
| 275 | close(); | ||
| 276 | mManager = null; | ||
| 277 | } | ||
| 278 | |||
| 279 | @Override | ||
| 280 | public void setFrozen(boolean frozen) { | ||
| 281 | mFrozen = frozen; | ||
| 282 | } | ||
| 283 | |||
| 284 | protected class InputThread extends Thread { | ||
| 285 | @Override | ||
| 286 | public void run() { | ||
| 287 | int packetSize = mInputEndpoint.getMaxPacketSize(); | ||
| 288 | byte[] packet = new byte[packetSize]; | ||
| 289 | while (mRunning) { | ||
| 290 | int r; | ||
| 291 | try | ||
| 292 | { | ||
| 293 | r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000); | ||
| 294 | } | ||
| 295 | catch (Exception e) | ||
| 296 | { | ||
| 297 | Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e); | ||
| 298 | break; | ||
| 299 | } | ||
| 300 | if (r < 0) { | ||
| 301 | // Could be a timeout or an I/O error | ||
| 302 | } | ||
| 303 | if (r > 0) { | ||
| 304 | byte[] data; | ||
| 305 | if (r == packetSize) { | ||
| 306 | data = packet; | ||
| 307 | } else { | ||
| 308 | data = Arrays.copyOfRange(packet, 0, r); | ||
| 309 | } | ||
| 310 | |||
| 311 | if (!mFrozen) { | ||
| 312 | mManager.HIDDeviceInputReport(mDeviceId, data); | ||
| 313 | } | ||
| 314 | } | ||
| 315 | } | ||
| 316 | } | ||
| 317 | } | ||
| 318 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDL.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDL.java new file mode 100644 index 0000000..b132fea --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDL.java | |||
| @@ -0,0 +1,90 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.content.Context; | ||
| 4 | |||
| 5 | import java.lang.Class; | ||
| 6 | import java.lang.reflect.Method; | ||
| 7 | |||
| 8 | /** | ||
| 9 | SDL library initialization | ||
| 10 | */ | ||
| 11 | public class SDL { | ||
| 12 | |||
| 13 | // This function should be called first and sets up the native code | ||
| 14 | // so it can call into the Java classes | ||
| 15 | public static void setupJNI() { | ||
| 16 | SDLActivity.nativeSetupJNI(); | ||
| 17 | SDLAudioManager.nativeSetupJNI(); | ||
| 18 | SDLControllerManager.nativeSetupJNI(); | ||
| 19 | } | ||
| 20 | |||
| 21 | // This function should be called each time the activity is started | ||
| 22 | public static void initialize() { | ||
| 23 | setContext(null); | ||
| 24 | |||
| 25 | SDLActivity.initialize(); | ||
| 26 | SDLAudioManager.initialize(); | ||
| 27 | SDLControllerManager.initialize(); | ||
| 28 | } | ||
| 29 | |||
| 30 | // This function stores the current activity (SDL or not) | ||
| 31 | public static void setContext(Context context) { | ||
| 32 | SDLAudioManager.setContext(context); | ||
| 33 | mContext = context; | ||
| 34 | } | ||
| 35 | |||
| 36 | public static Context getContext() { | ||
| 37 | return mContext; | ||
| 38 | } | ||
| 39 | |||
| 40 | public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { | ||
| 41 | loadLibrary(libraryName, mContext); | ||
| 42 | } | ||
| 43 | |||
| 44 | public static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException { | ||
| 45 | |||
| 46 | if (libraryName == null) { | ||
| 47 | throw new NullPointerException("No library name provided."); | ||
| 48 | } | ||
| 49 | |||
| 50 | try { | ||
| 51 | // Let's see if we have ReLinker available in the project. This is necessary for | ||
| 52 | // some projects that have huge numbers of local libraries bundled, and thus may | ||
| 53 | // trip a bug in Android's native library loader which ReLinker works around. (If | ||
| 54 | // loadLibrary works properly, ReLinker will simply use the normal Android method | ||
| 55 | // internally.) | ||
| 56 | // | ||
| 57 | // To use ReLinker, just add it as a dependency. For more information, see | ||
| 58 | // https://github.com/KeepSafe/ReLinker for ReLinker's repository. | ||
| 59 | // | ||
| 60 | Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker"); | ||
| 61 | Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener"); | ||
| 62 | Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context"); | ||
| 63 | Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String"); | ||
| 64 | |||
| 65 | // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if | ||
| 66 | // they've changed during updates. | ||
| 67 | Method forceMethod = relinkClass.getDeclaredMethod("force"); | ||
| 68 | Object relinkInstance = forceMethod.invoke(null); | ||
| 69 | Class<?> relinkInstanceClass = relinkInstance.getClass(); | ||
| 70 | |||
| 71 | // Actually load the library! | ||
| 72 | Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass); | ||
| 73 | loadMethod.invoke(relinkInstance, context, libraryName, null, null); | ||
| 74 | } | ||
| 75 | catch (final Throwable e) { | ||
| 76 | // Fall back | ||
| 77 | try { | ||
| 78 | System.loadLibrary(libraryName); | ||
| 79 | } | ||
| 80 | catch (final UnsatisfiedLinkError ule) { | ||
| 81 | throw ule; | ||
| 82 | } | ||
| 83 | catch (final SecurityException se) { | ||
| 84 | throw se; | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | protected static Context mContext; | ||
| 90 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java new file mode 100644 index 0000000..31aa9b6 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java | |||
| @@ -0,0 +1,2189 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.app.Activity; | ||
| 4 | import android.app.AlertDialog; | ||
| 5 | import android.app.Dialog; | ||
| 6 | import android.app.UiModeManager; | ||
| 7 | import android.content.ActivityNotFoundException; | ||
| 8 | import android.content.ClipboardManager; | ||
| 9 | import android.content.ClipData; | ||
| 10 | import android.content.Context; | ||
| 11 | import android.content.DialogInterface; | ||
| 12 | import android.content.Intent; | ||
| 13 | import android.content.pm.ActivityInfo; | ||
| 14 | import android.content.pm.ApplicationInfo; | ||
| 15 | import android.content.pm.PackageManager; | ||
| 16 | import android.content.res.Configuration; | ||
| 17 | import android.graphics.Bitmap; | ||
| 18 | import android.graphics.Color; | ||
| 19 | import android.graphics.PorterDuff; | ||
| 20 | import android.graphics.drawable.Drawable; | ||
| 21 | import android.hardware.Sensor; | ||
| 22 | import android.net.Uri; | ||
| 23 | import android.os.Build; | ||
| 24 | import android.os.Bundle; | ||
| 25 | import android.os.Handler; | ||
| 26 | import android.os.Message; | ||
| 27 | import android.os.ParcelFileDescriptor; | ||
| 28 | import android.util.DisplayMetrics; | ||
| 29 | import android.util.Log; | ||
| 30 | import android.util.SparseArray; | ||
| 31 | import android.view.Display; | ||
| 32 | import android.view.Gravity; | ||
| 33 | import android.view.InputDevice; | ||
| 34 | import android.view.KeyEvent; | ||
| 35 | import android.view.PointerIcon; | ||
| 36 | import android.view.Surface; | ||
| 37 | import android.view.View; | ||
| 38 | import android.view.ViewGroup; | ||
| 39 | import android.view.Window; | ||
| 40 | import android.view.WindowManager; | ||
| 41 | import android.view.inputmethod.InputConnection; | ||
| 42 | import android.view.inputmethod.InputMethodManager; | ||
| 43 | import android.webkit.MimeTypeMap; | ||
| 44 | import android.widget.Button; | ||
| 45 | import android.widget.LinearLayout; | ||
| 46 | import android.widget.RelativeLayout; | ||
| 47 | import android.widget.TextView; | ||
| 48 | import android.widget.Toast; | ||
| 49 | |||
| 50 | import java.io.FileNotFoundException; | ||
| 51 | import java.util.ArrayList; | ||
| 52 | import java.util.Hashtable; | ||
| 53 | import java.util.Locale; | ||
| 54 | |||
| 55 | |||
| 56 | /** | ||
| 57 | SDL Activity | ||
| 58 | */ | ||
| 59 | public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { | ||
| 60 | private static final String TAG = "SDL"; | ||
| 61 | private static final int SDL_MAJOR_VERSION = 3; | ||
| 62 | private static final int SDL_MINOR_VERSION = 2; | ||
| 63 | private static final int SDL_MICRO_VERSION = 8; | ||
| 64 | /* | ||
| 65 | // Display InputType.SOURCE/CLASS of events and devices | ||
| 66 | // | ||
| 67 | // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]"); | ||
| 68 | // SDLActivity.debugSource(event.getSource(), "event"); | ||
| 69 | public static void debugSource(int sources, String prefix) { | ||
| 70 | int s = sources; | ||
| 71 | int s_copy = sources; | ||
| 72 | String cls = ""; | ||
| 73 | String src = ""; | ||
| 74 | int tst = 0; | ||
| 75 | int FLAG_TAINTED = 0x80000000; | ||
| 76 | |||
| 77 | if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0) cls += " BUTTON"; | ||
| 78 | if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) cls += " JOYSTICK"; | ||
| 79 | if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0) cls += " POINTER"; | ||
| 80 | if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0) cls += " POSITION"; | ||
| 81 | if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) cls += " TRACKBALL"; | ||
| 82 | |||
| 83 | |||
| 84 | int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits | ||
| 85 | s2 &= ~( InputDevice.SOURCE_CLASS_BUTTON | ||
| 86 | | InputDevice.SOURCE_CLASS_JOYSTICK | ||
| 87 | | InputDevice.SOURCE_CLASS_POINTER | ||
| 88 | | InputDevice.SOURCE_CLASS_POSITION | ||
| 89 | | InputDevice.SOURCE_CLASS_TRACKBALL); | ||
| 90 | |||
| 91 | if (s2 != 0) cls += "Some_Unknown"; | ||
| 92 | |||
| 93 | s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; | ||
| 94 | |||
| 95 | if (Build.VERSION.SDK_INT >= 23) { | ||
| 96 | tst = InputDevice.SOURCE_BLUETOOTH_STYLUS; | ||
| 97 | if ((s & tst) == tst) src += " BLUETOOTH_STYLUS"; | ||
| 98 | s2 &= ~tst; | ||
| 99 | } | ||
| 100 | |||
| 101 | tst = InputDevice.SOURCE_DPAD; | ||
| 102 | if ((s & tst) == tst) src += " DPAD"; | ||
| 103 | s2 &= ~tst; | ||
| 104 | |||
| 105 | tst = InputDevice.SOURCE_GAMEPAD; | ||
| 106 | if ((s & tst) == tst) src += " GAMEPAD"; | ||
| 107 | s2 &= ~tst; | ||
| 108 | |||
| 109 | if (Build.VERSION.SDK_INT >= 21) { | ||
| 110 | tst = InputDevice.SOURCE_HDMI; | ||
| 111 | if ((s & tst) == tst) src += " HDMI"; | ||
| 112 | s2 &= ~tst; | ||
| 113 | } | ||
| 114 | |||
| 115 | tst = InputDevice.SOURCE_JOYSTICK; | ||
| 116 | if ((s & tst) == tst) src += " JOYSTICK"; | ||
| 117 | s2 &= ~tst; | ||
| 118 | |||
| 119 | tst = InputDevice.SOURCE_KEYBOARD; | ||
| 120 | if ((s & tst) == tst) src += " KEYBOARD"; | ||
| 121 | s2 &= ~tst; | ||
| 122 | |||
| 123 | tst = InputDevice.SOURCE_MOUSE; | ||
| 124 | if ((s & tst) == tst) src += " MOUSE"; | ||
| 125 | s2 &= ~tst; | ||
| 126 | |||
| 127 | if (Build.VERSION.SDK_INT >= 26) { | ||
| 128 | tst = InputDevice.SOURCE_MOUSE_RELATIVE; | ||
| 129 | if ((s & tst) == tst) src += " MOUSE_RELATIVE"; | ||
| 130 | s2 &= ~tst; | ||
| 131 | |||
| 132 | tst = InputDevice.SOURCE_ROTARY_ENCODER; | ||
| 133 | if ((s & tst) == tst) src += " ROTARY_ENCODER"; | ||
| 134 | s2 &= ~tst; | ||
| 135 | } | ||
| 136 | tst = InputDevice.SOURCE_STYLUS; | ||
| 137 | if ((s & tst) == tst) src += " STYLUS"; | ||
| 138 | s2 &= ~tst; | ||
| 139 | |||
| 140 | tst = InputDevice.SOURCE_TOUCHPAD; | ||
| 141 | if ((s & tst) == tst) src += " TOUCHPAD"; | ||
| 142 | s2 &= ~tst; | ||
| 143 | |||
| 144 | tst = InputDevice.SOURCE_TOUCHSCREEN; | ||
| 145 | if ((s & tst) == tst) src += " TOUCHSCREEN"; | ||
| 146 | s2 &= ~tst; | ||
| 147 | |||
| 148 | if (Build.VERSION.SDK_INT >= 18) { | ||
| 149 | tst = InputDevice.SOURCE_TOUCH_NAVIGATION; | ||
| 150 | if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; | ||
| 151 | s2 &= ~tst; | ||
| 152 | } | ||
| 153 | |||
| 154 | tst = InputDevice.SOURCE_TRACKBALL; | ||
| 155 | if ((s & tst) == tst) src += " TRACKBALL"; | ||
| 156 | s2 &= ~tst; | ||
| 157 | |||
| 158 | tst = InputDevice.SOURCE_ANY; | ||
| 159 | if ((s & tst) == tst) src += " ANY"; | ||
| 160 | s2 &= ~tst; | ||
| 161 | |||
| 162 | if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; | ||
| 163 | s2 &= ~FLAG_TAINTED; | ||
| 164 | |||
| 165 | if (s2 != 0) src += " Some_Unknown"; | ||
| 166 | |||
| 167 | Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); | ||
| 168 | } | ||
| 169 | */ | ||
| 170 | |||
| 171 | public static boolean mIsResumedCalled, mHasFocus; | ||
| 172 | public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */); | ||
| 173 | |||
| 174 | // Cursor types | ||
| 175 | // private static final int SDL_SYSTEM_CURSOR_NONE = -1; | ||
| 176 | private static final int SDL_SYSTEM_CURSOR_ARROW = 0; | ||
| 177 | private static final int SDL_SYSTEM_CURSOR_IBEAM = 1; | ||
| 178 | private static final int SDL_SYSTEM_CURSOR_WAIT = 2; | ||
| 179 | private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3; | ||
| 180 | private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4; | ||
| 181 | private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5; | ||
| 182 | private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6; | ||
| 183 | private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7; | ||
| 184 | private static final int SDL_SYSTEM_CURSOR_SIZENS = 8; | ||
| 185 | private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; | ||
| 186 | private static final int SDL_SYSTEM_CURSOR_NO = 10; | ||
| 187 | private static final int SDL_SYSTEM_CURSOR_HAND = 11; | ||
| 188 | private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT = 12; | ||
| 189 | private static final int SDL_SYSTEM_CURSOR_WINDOW_TOP = 13; | ||
| 190 | private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT = 14; | ||
| 191 | private static final int SDL_SYSTEM_CURSOR_WINDOW_RIGHT = 15; | ||
| 192 | private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT = 16; | ||
| 193 | private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOM = 17; | ||
| 194 | private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT = 18; | ||
| 195 | private static final int SDL_SYSTEM_CURSOR_WINDOW_LEFT = 19; | ||
| 196 | |||
| 197 | protected static final int SDL_ORIENTATION_UNKNOWN = 0; | ||
| 198 | protected static final int SDL_ORIENTATION_LANDSCAPE = 1; | ||
| 199 | protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2; | ||
| 200 | protected static final int SDL_ORIENTATION_PORTRAIT = 3; | ||
| 201 | protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; | ||
| 202 | |||
| 203 | protected static int mCurrentRotation; | ||
| 204 | protected static Locale mCurrentLocale; | ||
| 205 | |||
| 206 | // Handle the state of the native layer | ||
| 207 | public enum NativeState { | ||
| 208 | INIT, RESUMED, PAUSED | ||
| 209 | } | ||
| 210 | |||
| 211 | public static NativeState mNextNativeState; | ||
| 212 | public static NativeState mCurrentNativeState; | ||
| 213 | |||
| 214 | /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ | ||
| 215 | public static boolean mBrokenLibraries = true; | ||
| 216 | |||
| 217 | // Main components | ||
| 218 | protected static SDLActivity mSingleton; | ||
| 219 | protected static SDLSurface mSurface; | ||
| 220 | protected static SDLDummyEdit mTextEdit; | ||
| 221 | protected static boolean mScreenKeyboardShown; | ||
| 222 | protected static ViewGroup mLayout; | ||
| 223 | protected static SDLClipboardHandler mClipboardHandler; | ||
| 224 | protected static Hashtable<Integer, PointerIcon> mCursors; | ||
| 225 | protected static int mLastCursorID; | ||
| 226 | protected static SDLGenericMotionListener_API14 mMotionListener; | ||
| 227 | protected static HIDDeviceManager mHIDDeviceManager; | ||
| 228 | |||
| 229 | // This is what SDL runs in. It invokes SDL_main(), eventually | ||
| 230 | protected static Thread mSDLThread; | ||
| 231 | protected static boolean mSDLMainFinished = false; | ||
| 232 | protected static boolean mActivityCreated = false; | ||
| 233 | private static SDLFileDialogState mFileDialogState = null; | ||
| 234 | protected static boolean mDispatchingKeyEvent = false; | ||
| 235 | |||
| 236 | protected static SDLGenericMotionListener_API14 getMotionListener() { | ||
| 237 | if (mMotionListener == null) { | ||
| 238 | if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { | ||
| 239 | mMotionListener = new SDLGenericMotionListener_API26(); | ||
| 240 | } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 241 | mMotionListener = new SDLGenericMotionListener_API24(); | ||
| 242 | } else { | ||
| 243 | mMotionListener = new SDLGenericMotionListener_API14(); | ||
| 244 | } | ||
| 245 | } | ||
| 246 | |||
| 247 | return mMotionListener; | ||
| 248 | } | ||
| 249 | |||
| 250 | /** | ||
| 251 | * The application entry point, called on a dedicated thread (SDLThread). | ||
| 252 | * The default implementation uses the getMainSharedObject() and getMainFunction() methods | ||
| 253 | * to invoke native code from the specified shared library. | ||
| 254 | * It can be overridden by derived classes. | ||
| 255 | */ | ||
| 256 | protected void main() { | ||
| 257 | String library = SDLActivity.mSingleton.getMainSharedObject(); | ||
| 258 | String function = SDLActivity.mSingleton.getMainFunction(); | ||
| 259 | String[] arguments = SDLActivity.mSingleton.getArguments(); | ||
| 260 | |||
| 261 | Log.v("SDL", "Running main function " + function + " from library " + library); | ||
| 262 | SDLActivity.nativeRunMain(library, function, arguments); | ||
| 263 | Log.v("SDL", "Finished main function"); | ||
| 264 | } | ||
| 265 | |||
| 266 | /** | ||
| 267 | * This method returns the name of the shared object with the application entry point | ||
| 268 | * It can be overridden by derived classes. | ||
| 269 | */ | ||
| 270 | protected String getMainSharedObject() { | ||
| 271 | String library; | ||
| 272 | String[] libraries = SDLActivity.mSingleton.getLibraries(); | ||
| 273 | if (libraries.length > 0) { | ||
| 274 | library = "lib" + libraries[libraries.length - 1] + ".so"; | ||
| 275 | } else { | ||
| 276 | library = "libmain.so"; | ||
| 277 | } | ||
| 278 | return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; | ||
| 279 | } | ||
| 280 | |||
| 281 | /** | ||
| 282 | * This method returns the name of the application entry point | ||
| 283 | * It can be overridden by derived classes. | ||
| 284 | */ | ||
| 285 | protected String getMainFunction() { | ||
| 286 | return "SDL_main"; | ||
| 287 | } | ||
| 288 | |||
| 289 | /** | ||
| 290 | * This method is called by SDL before loading the native shared libraries. | ||
| 291 | * It can be overridden to provide names of shared libraries to be loaded. | ||
| 292 | * The default implementation returns the defaults. It never returns null. | ||
| 293 | * An array returned by a new implementation must at least contain "SDL3". | ||
| 294 | * Also keep in mind that the order the libraries are loaded may matter. | ||
| 295 | * @return names of shared libraries to be loaded (e.g. "SDL3", "main"). | ||
| 296 | */ | ||
| 297 | protected String[] getLibraries() { | ||
| 298 | return new String[] { | ||
| 299 | "SDL3", | ||
| 300 | // "SDL3_image", | ||
| 301 | // "SDL3_mixer", | ||
| 302 | // "SDL3_net", | ||
| 303 | // "SDL3_ttf", | ||
| 304 | "main" | ||
| 305 | }; | ||
| 306 | } | ||
| 307 | |||
| 308 | // Load the .so | ||
| 309 | public void loadLibraries() { | ||
| 310 | for (String lib : getLibraries()) { | ||
| 311 | SDL.loadLibrary(lib, this); | ||
| 312 | } | ||
| 313 | } | ||
| 314 | |||
| 315 | /** | ||
| 316 | * This method is called by SDL before starting the native application thread. | ||
| 317 | * It can be overridden to provide the arguments after the application name. | ||
| 318 | * The default implementation returns an empty array. It never returns null. | ||
| 319 | * @return arguments for the native application. | ||
| 320 | */ | ||
| 321 | protected String[] getArguments() { | ||
| 322 | return new String[0]; | ||
| 323 | } | ||
| 324 | |||
| 325 | public static void initialize() { | ||
| 326 | // The static nature of the singleton and Android quirkyness force us to initialize everything here | ||
| 327 | // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values | ||
| 328 | mSingleton = null; | ||
| 329 | mSurface = null; | ||
| 330 | mTextEdit = null; | ||
| 331 | mLayout = null; | ||
| 332 | mClipboardHandler = null; | ||
| 333 | mCursors = new Hashtable<Integer, PointerIcon>(); | ||
| 334 | mLastCursorID = 0; | ||
| 335 | mSDLThread = null; | ||
| 336 | mIsResumedCalled = false; | ||
| 337 | mHasFocus = true; | ||
| 338 | mNextNativeState = NativeState.INIT; | ||
| 339 | mCurrentNativeState = NativeState.INIT; | ||
| 340 | } | ||
| 341 | |||
| 342 | protected SDLSurface createSDLSurface(Context context) { | ||
| 343 | return new SDLSurface(context); | ||
| 344 | } | ||
| 345 | |||
| 346 | // Setup | ||
| 347 | @Override | ||
| 348 | protected void onCreate(Bundle savedInstanceState) { | ||
| 349 | Log.v(TAG, "Manufacturer: " + Build.MANUFACTURER); | ||
| 350 | Log.v(TAG, "Device: " + Build.DEVICE); | ||
| 351 | Log.v(TAG, "Model: " + Build.MODEL); | ||
| 352 | Log.v(TAG, "onCreate()"); | ||
| 353 | super.onCreate(savedInstanceState); | ||
| 354 | |||
| 355 | |||
| 356 | /* Control activity re-creation */ | ||
| 357 | if (mSDLMainFinished || mActivityCreated) { | ||
| 358 | boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); | ||
| 359 | if (mSDLMainFinished) { | ||
| 360 | Log.v(TAG, "SDL main() finished"); | ||
| 361 | } | ||
| 362 | if (allow_recreate) { | ||
| 363 | Log.v(TAG, "activity re-created"); | ||
| 364 | } else { | ||
| 365 | Log.v(TAG, "activity finished"); | ||
| 366 | System.exit(0); | ||
| 367 | return; | ||
| 368 | } | ||
| 369 | } | ||
| 370 | |||
| 371 | mActivityCreated = true; | ||
| 372 | |||
| 373 | try { | ||
| 374 | Thread.currentThread().setName("SDLActivity"); | ||
| 375 | } catch (Exception e) { | ||
| 376 | Log.v(TAG, "modify thread properties failed " + e.toString()); | ||
| 377 | } | ||
| 378 | |||
| 379 | // Load shared libraries | ||
| 380 | String errorMsgBrokenLib = ""; | ||
| 381 | try { | ||
| 382 | loadLibraries(); | ||
| 383 | mBrokenLibraries = false; /* success */ | ||
| 384 | } catch(UnsatisfiedLinkError e) { | ||
| 385 | System.err.println(e.getMessage()); | ||
| 386 | mBrokenLibraries = true; | ||
| 387 | errorMsgBrokenLib = e.getMessage(); | ||
| 388 | } catch(Exception e) { | ||
| 389 | System.err.println(e.getMessage()); | ||
| 390 | mBrokenLibraries = true; | ||
| 391 | errorMsgBrokenLib = e.getMessage(); | ||
| 392 | } | ||
| 393 | |||
| 394 | if (!mBrokenLibraries) { | ||
| 395 | String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." + | ||
| 396 | String.valueOf(SDL_MINOR_VERSION) + "." + | ||
| 397 | String.valueOf(SDL_MICRO_VERSION); | ||
| 398 | String version = nativeGetVersion(); | ||
| 399 | if (!version.equals(expected_version)) { | ||
| 400 | mBrokenLibraries = true; | ||
| 401 | errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")"; | ||
| 402 | } | ||
| 403 | } | ||
| 404 | |||
| 405 | if (mBrokenLibraries) { | ||
| 406 | mSingleton = this; | ||
| 407 | AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); | ||
| 408 | dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." | ||
| 409 | + System.getProperty("line.separator") | ||
| 410 | + System.getProperty("line.separator") | ||
| 411 | + "Error: " + errorMsgBrokenLib); | ||
| 412 | dlgAlert.setTitle("SDL Error"); | ||
| 413 | dlgAlert.setPositiveButton("Exit", | ||
| 414 | new DialogInterface.OnClickListener() { | ||
| 415 | @Override | ||
| 416 | public void onClick(DialogInterface dialog,int id) { | ||
| 417 | // if this button is clicked, close current activity | ||
| 418 | SDLActivity.mSingleton.finish(); | ||
| 419 | } | ||
| 420 | }); | ||
| 421 | dlgAlert.setCancelable(false); | ||
| 422 | dlgAlert.create().show(); | ||
| 423 | |||
| 424 | return; | ||
| 425 | } | ||
| 426 | |||
| 427 | |||
| 428 | /* Control activity re-creation */ | ||
| 429 | /* Robustness: check that the native code is run for the first time. | ||
| 430 | * (Maybe Activity was reset, but not the native code.) */ | ||
| 431 | { | ||
| 432 | int run_count = SDLActivity.nativeCheckSDLThreadCounter(); /* get and increment a native counter */ | ||
| 433 | if (run_count != 0) { | ||
| 434 | boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); | ||
| 435 | if (allow_recreate) { | ||
| 436 | Log.v(TAG, "activity re-created // run_count: " + run_count); | ||
| 437 | } else { | ||
| 438 | Log.v(TAG, "activity finished // run_count: " + run_count); | ||
| 439 | System.exit(0); | ||
| 440 | return; | ||
| 441 | } | ||
| 442 | } | ||
| 443 | } | ||
| 444 | |||
| 445 | // Set up JNI | ||
| 446 | SDL.setupJNI(); | ||
| 447 | |||
| 448 | // Initialize state | ||
| 449 | SDL.initialize(); | ||
| 450 | |||
| 451 | // So we can call stuff from static callbacks | ||
| 452 | mSingleton = this; | ||
| 453 | SDL.setContext(this); | ||
| 454 | |||
| 455 | mClipboardHandler = new SDLClipboardHandler(); | ||
| 456 | |||
| 457 | mHIDDeviceManager = HIDDeviceManager.acquire(this); | ||
| 458 | |||
| 459 | // Set up the surface | ||
| 460 | mSurface = createSDLSurface(this); | ||
| 461 | |||
| 462 | mLayout = new RelativeLayout(this); | ||
| 463 | mLayout.addView(mSurface); | ||
| 464 | |||
| 465 | // Get our current screen orientation and pass it down. | ||
| 466 | SDLActivity.nativeSetNaturalOrientation(SDLActivity.getNaturalOrientation()); | ||
| 467 | mCurrentRotation = SDLActivity.getCurrentRotation(); | ||
| 468 | SDLActivity.onNativeRotationChanged(mCurrentRotation); | ||
| 469 | |||
| 470 | try { | ||
| 471 | if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { | ||
| 472 | mCurrentLocale = getContext().getResources().getConfiguration().locale; | ||
| 473 | } else { | ||
| 474 | mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); | ||
| 475 | } | ||
| 476 | } catch(Exception ignored) { | ||
| 477 | } | ||
| 478 | |||
| 479 | switch (getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) { | ||
| 480 | case Configuration.UI_MODE_NIGHT_NO: | ||
| 481 | SDLActivity.onNativeDarkModeChanged(false); | ||
| 482 | break; | ||
| 483 | case Configuration.UI_MODE_NIGHT_YES: | ||
| 484 | SDLActivity.onNativeDarkModeChanged(true); | ||
| 485 | break; | ||
| 486 | } | ||
| 487 | |||
| 488 | setContentView(mLayout); | ||
| 489 | |||
| 490 | setWindowStyle(false); | ||
| 491 | |||
| 492 | getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); | ||
| 493 | |||
| 494 | // Get filename from "Open with" of another application | ||
| 495 | Intent intent = getIntent(); | ||
| 496 | if (intent != null && intent.getData() != null) { | ||
| 497 | String filename = intent.getData().getPath(); | ||
| 498 | if (filename != null) { | ||
| 499 | Log.v(TAG, "Got filename: " + filename); | ||
| 500 | SDLActivity.onNativeDropFile(filename); | ||
| 501 | } | ||
| 502 | } | ||
| 503 | } | ||
| 504 | |||
| 505 | protected void pauseNativeThread() { | ||
| 506 | mNextNativeState = NativeState.PAUSED; | ||
| 507 | mIsResumedCalled = false; | ||
| 508 | |||
| 509 | if (SDLActivity.mBrokenLibraries) { | ||
| 510 | return; | ||
| 511 | } | ||
| 512 | |||
| 513 | SDLActivity.handleNativeState(); | ||
| 514 | } | ||
| 515 | |||
| 516 | protected void resumeNativeThread() { | ||
| 517 | mNextNativeState = NativeState.RESUMED; | ||
| 518 | mIsResumedCalled = true; | ||
| 519 | |||
| 520 | if (SDLActivity.mBrokenLibraries) { | ||
| 521 | return; | ||
| 522 | } | ||
| 523 | |||
| 524 | SDLActivity.handleNativeState(); | ||
| 525 | } | ||
| 526 | |||
| 527 | // Events | ||
| 528 | @Override | ||
| 529 | protected void onPause() { | ||
| 530 | Log.v(TAG, "onPause()"); | ||
| 531 | super.onPause(); | ||
| 532 | |||
| 533 | if (mHIDDeviceManager != null) { | ||
| 534 | mHIDDeviceManager.setFrozen(true); | ||
| 535 | } | ||
| 536 | if (!mHasMultiWindow) { | ||
| 537 | pauseNativeThread(); | ||
| 538 | } | ||
| 539 | } | ||
| 540 | |||
| 541 | @Override | ||
| 542 | protected void onResume() { | ||
| 543 | Log.v(TAG, "onResume()"); | ||
| 544 | super.onResume(); | ||
| 545 | |||
| 546 | if (mHIDDeviceManager != null) { | ||
| 547 | mHIDDeviceManager.setFrozen(false); | ||
| 548 | } | ||
| 549 | if (!mHasMultiWindow) { | ||
| 550 | resumeNativeThread(); | ||
| 551 | } | ||
| 552 | } | ||
| 553 | |||
| 554 | @Override | ||
| 555 | protected void onStop() { | ||
| 556 | Log.v(TAG, "onStop()"); | ||
| 557 | super.onStop(); | ||
| 558 | if (mHasMultiWindow) { | ||
| 559 | pauseNativeThread(); | ||
| 560 | } | ||
| 561 | } | ||
| 562 | |||
| 563 | @Override | ||
| 564 | protected void onStart() { | ||
| 565 | Log.v(TAG, "onStart()"); | ||
| 566 | super.onStart(); | ||
| 567 | if (mHasMultiWindow) { | ||
| 568 | resumeNativeThread(); | ||
| 569 | } | ||
| 570 | } | ||
| 571 | |||
| 572 | public static int getNaturalOrientation() { | ||
| 573 | int result = SDL_ORIENTATION_UNKNOWN; | ||
| 574 | |||
| 575 | Activity activity = (Activity)getContext(); | ||
| 576 | if (activity != null) { | ||
| 577 | Configuration config = activity.getResources().getConfiguration(); | ||
| 578 | Display display = activity.getWindowManager().getDefaultDisplay(); | ||
| 579 | int rotation = display.getRotation(); | ||
| 580 | if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) && | ||
| 581 | config.orientation == Configuration.ORIENTATION_LANDSCAPE) || | ||
| 582 | ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) && | ||
| 583 | config.orientation == Configuration.ORIENTATION_PORTRAIT)) { | ||
| 584 | result = SDL_ORIENTATION_LANDSCAPE; | ||
| 585 | } else { | ||
| 586 | result = SDL_ORIENTATION_PORTRAIT; | ||
| 587 | } | ||
| 588 | } | ||
| 589 | return result; | ||
| 590 | } | ||
| 591 | |||
| 592 | public static int getCurrentRotation() { | ||
| 593 | int result = 0; | ||
| 594 | |||
| 595 | Activity activity = (Activity)getContext(); | ||
| 596 | if (activity != null) { | ||
| 597 | Display display = activity.getWindowManager().getDefaultDisplay(); | ||
| 598 | switch (display.getRotation()) { | ||
| 599 | case Surface.ROTATION_0: | ||
| 600 | result = 0; | ||
| 601 | break; | ||
| 602 | case Surface.ROTATION_90: | ||
| 603 | result = 90; | ||
| 604 | break; | ||
| 605 | case Surface.ROTATION_180: | ||
| 606 | result = 180; | ||
| 607 | break; | ||
| 608 | case Surface.ROTATION_270: | ||
| 609 | result = 270; | ||
| 610 | break; | ||
| 611 | } | ||
| 612 | } | ||
| 613 | return result; | ||
| 614 | } | ||
| 615 | |||
| 616 | @Override | ||
| 617 | public void onWindowFocusChanged(boolean hasFocus) { | ||
| 618 | super.onWindowFocusChanged(hasFocus); | ||
| 619 | Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); | ||
| 620 | |||
| 621 | if (SDLActivity.mBrokenLibraries) { | ||
| 622 | return; | ||
| 623 | } | ||
| 624 | |||
| 625 | mHasFocus = hasFocus; | ||
| 626 | if (hasFocus) { | ||
| 627 | mNextNativeState = NativeState.RESUMED; | ||
| 628 | SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded(); | ||
| 629 | |||
| 630 | SDLActivity.handleNativeState(); | ||
| 631 | nativeFocusChanged(true); | ||
| 632 | |||
| 633 | } else { | ||
| 634 | nativeFocusChanged(false); | ||
| 635 | if (!mHasMultiWindow) { | ||
| 636 | mNextNativeState = NativeState.PAUSED; | ||
| 637 | SDLActivity.handleNativeState(); | ||
| 638 | } | ||
| 639 | } | ||
| 640 | } | ||
| 641 | |||
| 642 | @Override | ||
| 643 | public void onTrimMemory(int level) { | ||
| 644 | Log.v(TAG, "onTrimMemory()"); | ||
| 645 | super.onTrimMemory(level); | ||
| 646 | |||
| 647 | if (SDLActivity.mBrokenLibraries) { | ||
| 648 | return; | ||
| 649 | } | ||
| 650 | |||
| 651 | SDLActivity.nativeLowMemory(); | ||
| 652 | } | ||
| 653 | |||
| 654 | @Override | ||
| 655 | public void onConfigurationChanged(Configuration newConfig) { | ||
| 656 | Log.v(TAG, "onConfigurationChanged()"); | ||
| 657 | super.onConfigurationChanged(newConfig); | ||
| 658 | |||
| 659 | if (SDLActivity.mBrokenLibraries) { | ||
| 660 | return; | ||
| 661 | } | ||
| 662 | |||
| 663 | if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) { | ||
| 664 | mCurrentLocale = newConfig.locale; | ||
| 665 | SDLActivity.onNativeLocaleChanged(); | ||
| 666 | } | ||
| 667 | |||
| 668 | switch (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) { | ||
| 669 | case Configuration.UI_MODE_NIGHT_NO: | ||
| 670 | SDLActivity.onNativeDarkModeChanged(false); | ||
| 671 | break; | ||
| 672 | case Configuration.UI_MODE_NIGHT_YES: | ||
| 673 | SDLActivity.onNativeDarkModeChanged(true); | ||
| 674 | break; | ||
| 675 | } | ||
| 676 | } | ||
| 677 | |||
| 678 | @Override | ||
| 679 | protected void onDestroy() { | ||
| 680 | Log.v(TAG, "onDestroy()"); | ||
| 681 | |||
| 682 | if (mHIDDeviceManager != null) { | ||
| 683 | HIDDeviceManager.release(mHIDDeviceManager); | ||
| 684 | mHIDDeviceManager = null; | ||
| 685 | } | ||
| 686 | |||
| 687 | SDLAudioManager.release(this); | ||
| 688 | |||
| 689 | if (SDLActivity.mBrokenLibraries) { | ||
| 690 | super.onDestroy(); | ||
| 691 | return; | ||
| 692 | } | ||
| 693 | |||
| 694 | if (SDLActivity.mSDLThread != null) { | ||
| 695 | |||
| 696 | // Send Quit event to "SDLThread" thread | ||
| 697 | SDLActivity.nativeSendQuit(); | ||
| 698 | |||
| 699 | // Wait for "SDLThread" thread to end | ||
| 700 | try { | ||
| 701 | // Use a timeout because: | ||
| 702 | // C SDLmain() thread might have started (mSDLThread.start() called) | ||
| 703 | // while the SDL_Init() might not have been called yet, | ||
| 704 | // and so the previous QUIT event will be discarded by SDL_Init() and app is running, not exiting. | ||
| 705 | SDLActivity.mSDLThread.join(1000); | ||
| 706 | } catch(Exception e) { | ||
| 707 | Log.v(TAG, "Problem stopping SDLThread: " + e); | ||
| 708 | } | ||
| 709 | } | ||
| 710 | |||
| 711 | SDLActivity.nativeQuit(); | ||
| 712 | |||
| 713 | super.onDestroy(); | ||
| 714 | } | ||
| 715 | |||
| 716 | @Override | ||
| 717 | public void onBackPressed() { | ||
| 718 | // Check if we want to block the back button in case of mouse right click. | ||
| 719 | // | ||
| 720 | // If we do, the normal hardware back button will no longer work and people have to use home, | ||
| 721 | // but the mouse right click will work. | ||
| 722 | // | ||
| 723 | boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false); | ||
| 724 | if (trapBack) { | ||
| 725 | // Exit and let the mouse handler handle this button (if appropriate) | ||
| 726 | return; | ||
| 727 | } | ||
| 728 | |||
| 729 | // Default system back button behavior. | ||
| 730 | if (!isFinishing()) { | ||
| 731 | super.onBackPressed(); | ||
| 732 | } | ||
| 733 | } | ||
| 734 | |||
| 735 | @Override | ||
| 736 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||
| 737 | super.onActivityResult(requestCode, resultCode, data); | ||
| 738 | |||
| 739 | if (mFileDialogState != null && mFileDialogState.requestCode == requestCode) { | ||
| 740 | /* This is our file dialog */ | ||
| 741 | String[] filelist = null; | ||
| 742 | |||
| 743 | if (data != null) { | ||
| 744 | Uri singleFileUri = data.getData(); | ||
| 745 | |||
| 746 | if (singleFileUri == null) { | ||
| 747 | /* Use Intent.getClipData to get multiple choices */ | ||
| 748 | ClipData clipData = data.getClipData(); | ||
| 749 | assert clipData != null; | ||
| 750 | |||
| 751 | filelist = new String[clipData.getItemCount()]; | ||
| 752 | |||
| 753 | for (int i = 0; i < filelist.length; i++) { | ||
| 754 | String uri = clipData.getItemAt(i).getUri().toString(); | ||
| 755 | filelist[i] = uri; | ||
| 756 | } | ||
| 757 | } else { | ||
| 758 | /* Only one file is selected. */ | ||
| 759 | filelist = new String[]{singleFileUri.toString()}; | ||
| 760 | } | ||
| 761 | } else { | ||
| 762 | /* User cancelled the request. */ | ||
| 763 | filelist = new String[0]; | ||
| 764 | } | ||
| 765 | |||
| 766 | // TODO: Detect the file MIME type and pass the filter value accordingly. | ||
| 767 | SDLActivity.onNativeFileDialog(requestCode, filelist, -1); | ||
| 768 | mFileDialogState = null; | ||
| 769 | } | ||
| 770 | } | ||
| 771 | |||
| 772 | // Called by JNI from SDL. | ||
| 773 | public static void manualBackButton() { | ||
| 774 | mSingleton.pressBackButton(); | ||
| 775 | } | ||
| 776 | |||
| 777 | // Used to get us onto the activity's main thread | ||
| 778 | public void pressBackButton() { | ||
| 779 | runOnUiThread(new Runnable() { | ||
| 780 | @Override | ||
| 781 | public void run() { | ||
| 782 | if (!SDLActivity.this.isFinishing()) { | ||
| 783 | SDLActivity.this.superOnBackPressed(); | ||
| 784 | } | ||
| 785 | } | ||
| 786 | }); | ||
| 787 | } | ||
| 788 | |||
| 789 | // Used to access the system back behavior. | ||
| 790 | public void superOnBackPressed() { | ||
| 791 | super.onBackPressed(); | ||
| 792 | } | ||
| 793 | |||
| 794 | @Override | ||
| 795 | public boolean dispatchKeyEvent(KeyEvent event) { | ||
| 796 | |||
| 797 | if (SDLActivity.mBrokenLibraries) { | ||
| 798 | return false; | ||
| 799 | } | ||
| 800 | |||
| 801 | int keyCode = event.getKeyCode(); | ||
| 802 | // Ignore certain special keys so they're handled by Android | ||
| 803 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || | ||
| 804 | keyCode == KeyEvent.KEYCODE_VOLUME_UP || | ||
| 805 | keyCode == KeyEvent.KEYCODE_CAMERA || | ||
| 806 | keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ | ||
| 807 | keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ | ||
| 808 | ) { | ||
| 809 | return false; | ||
| 810 | } | ||
| 811 | mDispatchingKeyEvent = true; | ||
| 812 | boolean result = super.dispatchKeyEvent(event); | ||
| 813 | mDispatchingKeyEvent = false; | ||
| 814 | return result; | ||
| 815 | } | ||
| 816 | |||
| 817 | public static boolean dispatchingKeyEvent() { | ||
| 818 | return mDispatchingKeyEvent; | ||
| 819 | } | ||
| 820 | |||
| 821 | /* Transition to next state */ | ||
| 822 | public static void handleNativeState() { | ||
| 823 | |||
| 824 | if (mNextNativeState == mCurrentNativeState) { | ||
| 825 | // Already in same state, discard. | ||
| 826 | return; | ||
| 827 | } | ||
| 828 | |||
| 829 | // Try a transition to init state | ||
| 830 | if (mNextNativeState == NativeState.INIT) { | ||
| 831 | |||
| 832 | mCurrentNativeState = mNextNativeState; | ||
| 833 | return; | ||
| 834 | } | ||
| 835 | |||
| 836 | // Try a transition to paused state | ||
| 837 | if (mNextNativeState == NativeState.PAUSED) { | ||
| 838 | if (mSDLThread != null) { | ||
| 839 | nativePause(); | ||
| 840 | } | ||
| 841 | if (mSurface != null) { | ||
| 842 | mSurface.handlePause(); | ||
| 843 | } | ||
| 844 | mCurrentNativeState = mNextNativeState; | ||
| 845 | return; | ||
| 846 | } | ||
| 847 | |||
| 848 | // Try a transition to resumed state | ||
| 849 | if (mNextNativeState == NativeState.RESUMED) { | ||
| 850 | if (mSurface.mIsSurfaceReady && (mHasFocus || mHasMultiWindow) && mIsResumedCalled) { | ||
| 851 | if (mSDLThread == null) { | ||
| 852 | // This is the entry point to the C app. | ||
| 853 | // Start up the C app thread and enable sensor input for the first time | ||
| 854 | // FIXME: Why aren't we enabling sensor input at start? | ||
| 855 | |||
| 856 | mSDLThread = new Thread(new SDLMain(), "SDLThread"); | ||
| 857 | mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); | ||
| 858 | mSDLThread.start(); | ||
| 859 | |||
| 860 | // No nativeResume(), don't signal Android_ResumeSem | ||
| 861 | } else { | ||
| 862 | nativeResume(); | ||
| 863 | } | ||
| 864 | mSurface.handleResume(); | ||
| 865 | |||
| 866 | mCurrentNativeState = mNextNativeState; | ||
| 867 | } | ||
| 868 | } | ||
| 869 | } | ||
| 870 | |||
| 871 | // Messages from the SDLMain thread | ||
| 872 | protected static final int COMMAND_CHANGE_TITLE = 1; | ||
| 873 | protected static final int COMMAND_CHANGE_WINDOW_STYLE = 2; | ||
| 874 | protected static final int COMMAND_TEXTEDIT_HIDE = 3; | ||
| 875 | protected static final int COMMAND_SET_KEEP_SCREEN_ON = 5; | ||
| 876 | protected static final int COMMAND_USER = 0x8000; | ||
| 877 | |||
| 878 | protected static boolean mFullscreenModeActive; | ||
| 879 | |||
| 880 | /** | ||
| 881 | * This method is called by SDL if SDL did not handle a message itself. | ||
| 882 | * This happens if a received message contains an unsupported command. | ||
| 883 | * Method can be overwritten to handle Messages in a different class. | ||
| 884 | * @param command the command of the message. | ||
| 885 | * @param param the parameter of the message. May be null. | ||
| 886 | * @return if the message was handled in overridden method. | ||
| 887 | */ | ||
| 888 | protected boolean onUnhandledMessage(int command, Object param) { | ||
| 889 | return false; | ||
| 890 | } | ||
| 891 | |||
| 892 | /** | ||
| 893 | * A Handler class for Messages from native SDL applications. | ||
| 894 | * It uses current Activities as target (e.g. for the title). | ||
| 895 | * static to prevent implicit references to enclosing object. | ||
| 896 | */ | ||
| 897 | protected static class SDLCommandHandler extends Handler { | ||
| 898 | @Override | ||
| 899 | public void handleMessage(Message msg) { | ||
| 900 | Context context = SDL.getContext(); | ||
| 901 | if (context == null) { | ||
| 902 | Log.e(TAG, "error handling message, getContext() returned null"); | ||
| 903 | return; | ||
| 904 | } | ||
| 905 | switch (msg.arg1) { | ||
| 906 | case COMMAND_CHANGE_TITLE: | ||
| 907 | if (context instanceof Activity) { | ||
| 908 | ((Activity) context).setTitle((String)msg.obj); | ||
| 909 | } else { | ||
| 910 | Log.e(TAG, "error handling message, getContext() returned no Activity"); | ||
| 911 | } | ||
| 912 | break; | ||
| 913 | case COMMAND_CHANGE_WINDOW_STYLE: | ||
| 914 | if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { | ||
| 915 | if (context instanceof Activity) { | ||
| 916 | Window window = ((Activity) context).getWindow(); | ||
| 917 | if (window != null) { | ||
| 918 | if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { | ||
| 919 | int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | | ||
| 920 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | | ||
| 921 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | | ||
| 922 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | | ||
| 923 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | | ||
| 924 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; | ||
| 925 | window.getDecorView().setSystemUiVisibility(flags); | ||
| 926 | window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); | ||
| 927 | window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); | ||
| 928 | SDLActivity.mFullscreenModeActive = true; | ||
| 929 | } else { | ||
| 930 | int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; | ||
| 931 | window.getDecorView().setSystemUiVisibility(flags); | ||
| 932 | window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); | ||
| 933 | window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); | ||
| 934 | SDLActivity.mFullscreenModeActive = false; | ||
| 935 | } | ||
| 936 | if (Build.VERSION.SDK_INT >= 28 /* Android 9 (Pie) */) { | ||
| 937 | window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; | ||
| 938 | } | ||
| 939 | if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */ && | ||
| 940 | Build.VERSION.SDK_INT < 35 /* Android 15 */) { | ||
| 941 | SDLActivity.onNativeInsetsChanged(0, 0, 0, 0); | ||
| 942 | } | ||
| 943 | } | ||
| 944 | } else { | ||
| 945 | Log.e(TAG, "error handling message, getContext() returned no Activity"); | ||
| 946 | } | ||
| 947 | } | ||
| 948 | break; | ||
| 949 | case COMMAND_TEXTEDIT_HIDE: | ||
| 950 | if (mTextEdit != null) { | ||
| 951 | // Note: On some devices setting view to GONE creates a flicker in landscape. | ||
| 952 | // Setting the View's sizes to 0 is similar to GONE but without the flicker. | ||
| 953 | // The sizes will be set to useful values when the keyboard is shown again. | ||
| 954 | mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); | ||
| 955 | |||
| 956 | InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); | ||
| 957 | imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); | ||
| 958 | |||
| 959 | mScreenKeyboardShown = false; | ||
| 960 | |||
| 961 | mSurface.requestFocus(); | ||
| 962 | } | ||
| 963 | break; | ||
| 964 | case COMMAND_SET_KEEP_SCREEN_ON: | ||
| 965 | { | ||
| 966 | if (context instanceof Activity) { | ||
| 967 | Window window = ((Activity) context).getWindow(); | ||
| 968 | if (window != null) { | ||
| 969 | if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { | ||
| 970 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); | ||
| 971 | } else { | ||
| 972 | window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); | ||
| 973 | } | ||
| 974 | } | ||
| 975 | } | ||
| 976 | break; | ||
| 977 | } | ||
| 978 | default: | ||
| 979 | if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { | ||
| 980 | Log.e(TAG, "error handling message, command is " + msg.arg1); | ||
| 981 | } | ||
| 982 | } | ||
| 983 | } | ||
| 984 | } | ||
| 985 | |||
| 986 | // Handler for the messages | ||
| 987 | Handler commandHandler = new SDLCommandHandler(); | ||
| 988 | |||
| 989 | // Send a message from the SDLMain thread | ||
| 990 | protected boolean sendCommand(int command, Object data) { | ||
| 991 | Message msg = commandHandler.obtainMessage(); | ||
| 992 | msg.arg1 = command; | ||
| 993 | msg.obj = data; | ||
| 994 | boolean result = commandHandler.sendMessage(msg); | ||
| 995 | |||
| 996 | if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { | ||
| 997 | if (command == COMMAND_CHANGE_WINDOW_STYLE) { | ||
| 998 | // Ensure we don't return until the resize has actually happened, | ||
| 999 | // or 500ms have passed. | ||
| 1000 | |||
| 1001 | boolean bShouldWait = false; | ||
| 1002 | |||
| 1003 | if (data instanceof Integer) { | ||
| 1004 | // Let's figure out if we're already laid out fullscreen or not. | ||
| 1005 | Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); | ||
| 1006 | DisplayMetrics realMetrics = new DisplayMetrics(); | ||
| 1007 | display.getRealMetrics(realMetrics); | ||
| 1008 | |||
| 1009 | boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && | ||
| 1010 | (realMetrics.heightPixels == mSurface.getHeight())); | ||
| 1011 | |||
| 1012 | if ((Integer) data == 1) { | ||
| 1013 | // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going | ||
| 1014 | // to change size and should wait for surfaceChanged() before we return, so the size | ||
| 1015 | // is right back in native code. If we're already laid out fullscreen, though, we're | ||
| 1016 | // not going to change size even if we change decor modes, so we shouldn't wait for | ||
| 1017 | // surfaceChanged() -- which may not even happen -- and should return immediately. | ||
| 1018 | bShouldWait = !bFullscreenLayout; | ||
| 1019 | } else { | ||
| 1020 | // If we're laid out fullscreen (even if the status bar and nav bar are present), | ||
| 1021 | // or are actively in fullscreen, we're going to change size and should wait for | ||
| 1022 | // surfaceChanged before we return, so the size is right back in native code. | ||
| 1023 | bShouldWait = bFullscreenLayout; | ||
| 1024 | } | ||
| 1025 | } | ||
| 1026 | |||
| 1027 | if (bShouldWait && (SDLActivity.getContext() != null)) { | ||
| 1028 | // We'll wait for the surfaceChanged() method, which will notify us | ||
| 1029 | // when called. That way, we know our current size is really the | ||
| 1030 | // size we need, instead of grabbing a size that's still got | ||
| 1031 | // the navigation and/or status bars before they're hidden. | ||
| 1032 | // | ||
| 1033 | // We'll wait for up to half a second, because some devices | ||
| 1034 | // take a surprisingly long time for the surface resize, but | ||
| 1035 | // then we'll just give up and return. | ||
| 1036 | // | ||
| 1037 | synchronized (SDLActivity.getContext()) { | ||
| 1038 | try { | ||
| 1039 | SDLActivity.getContext().wait(500); | ||
| 1040 | } catch (InterruptedException ie) { | ||
| 1041 | ie.printStackTrace(); | ||
| 1042 | } | ||
| 1043 | } | ||
| 1044 | } | ||
| 1045 | } | ||
| 1046 | } | ||
| 1047 | |||
| 1048 | return result; | ||
| 1049 | } | ||
| 1050 | |||
| 1051 | // C functions we call | ||
| 1052 | public static native String nativeGetVersion(); | ||
| 1053 | public static native int nativeSetupJNI(); | ||
| 1054 | public static native void nativeInitMainThread(); | ||
| 1055 | public static native void nativeCleanupMainThread(); | ||
| 1056 | public static native int nativeRunMain(String library, String function, Object arguments); | ||
| 1057 | public static native void nativeLowMemory(); | ||
| 1058 | public static native void nativeSendQuit(); | ||
| 1059 | public static native void nativeQuit(); | ||
| 1060 | public static native void nativePause(); | ||
| 1061 | public static native void nativeResume(); | ||
| 1062 | public static native void nativeFocusChanged(boolean hasFocus); | ||
| 1063 | public static native void onNativeDropFile(String filename); | ||
| 1064 | public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float density, float rate); | ||
| 1065 | public static native void onNativeResize(); | ||
| 1066 | public static native void onNativeKeyDown(int keycode); | ||
| 1067 | public static native void onNativeKeyUp(int keycode); | ||
| 1068 | public static native boolean onNativeSoftReturnKey(); | ||
| 1069 | public static native void onNativeKeyboardFocusLost(); | ||
| 1070 | public static native void onNativeMouse(int button, int action, float x, float y, boolean relative); | ||
| 1071 | public static native void onNativeTouch(int touchDevId, int pointerFingerId, | ||
| 1072 | int action, float x, | ||
| 1073 | float y, float p); | ||
| 1074 | public static native void onNativePen(int penId, int button, int action, float x, float y, float p); | ||
| 1075 | public static native void onNativeAccel(float x, float y, float z); | ||
| 1076 | public static native void onNativeClipboardChanged(); | ||
| 1077 | public static native void onNativeSurfaceCreated(); | ||
| 1078 | public static native void onNativeSurfaceChanged(); | ||
| 1079 | public static native void onNativeSurfaceDestroyed(); | ||
| 1080 | public static native String nativeGetHint(String name); | ||
| 1081 | public static native boolean nativeGetHintBoolean(String name, boolean default_value); | ||
| 1082 | public static native void nativeSetenv(String name, String value); | ||
| 1083 | public static native void nativeSetNaturalOrientation(int orientation); | ||
| 1084 | public static native void onNativeRotationChanged(int rotation); | ||
| 1085 | public static native void onNativeInsetsChanged(int left, int right, int top, int bottom); | ||
| 1086 | public static native void nativeAddTouch(int touchId, String name); | ||
| 1087 | public static native void nativePermissionResult(int requestCode, boolean result); | ||
| 1088 | public static native void onNativeLocaleChanged(); | ||
| 1089 | public static native void onNativeDarkModeChanged(boolean enabled); | ||
| 1090 | public static native boolean nativeAllowRecreateActivity(); | ||
| 1091 | public static native int nativeCheckSDLThreadCounter(); | ||
| 1092 | public static native void onNativeFileDialog(int requestCode, String[] filelist, int filter); | ||
| 1093 | |||
| 1094 | /** | ||
| 1095 | * This method is called by SDL using JNI. | ||
| 1096 | */ | ||
| 1097 | public static boolean setActivityTitle(String title) { | ||
| 1098 | // Called from SDLMain() thread and can't directly affect the view | ||
| 1099 | return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); | ||
| 1100 | } | ||
| 1101 | |||
| 1102 | /** | ||
| 1103 | * This method is called by SDL using JNI. | ||
| 1104 | */ | ||
| 1105 | public static void setWindowStyle(boolean fullscreen) { | ||
| 1106 | // Called from SDLMain() thread and can't directly affect the view | ||
| 1107 | mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0); | ||
| 1108 | } | ||
| 1109 | |||
| 1110 | /** | ||
| 1111 | * This method is called by SDL using JNI. | ||
| 1112 | * This is a static method for JNI convenience, it calls a non-static method | ||
| 1113 | * so that is can be overridden | ||
| 1114 | */ | ||
| 1115 | public static void setOrientation(int w, int h, boolean resizable, String hint) | ||
| 1116 | { | ||
| 1117 | if (mSingleton != null) { | ||
| 1118 | mSingleton.setOrientationBis(w, h, resizable, hint); | ||
| 1119 | } | ||
| 1120 | } | ||
| 1121 | |||
| 1122 | /** | ||
| 1123 | * This can be overridden | ||
| 1124 | */ | ||
| 1125 | public void setOrientationBis(int w, int h, boolean resizable, String hint) | ||
| 1126 | { | ||
| 1127 | int orientation_landscape = -1; | ||
| 1128 | int orientation_portrait = -1; | ||
| 1129 | |||
| 1130 | /* If set, hint "explicitly controls which UI orientations are allowed". */ | ||
| 1131 | if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { | ||
| 1132 | orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; | ||
| 1133 | } else if (hint.contains("LandscapeLeft")) { | ||
| 1134 | orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; | ||
| 1135 | } else if (hint.contains("LandscapeRight")) { | ||
| 1136 | orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; | ||
| 1137 | } | ||
| 1138 | |||
| 1139 | /* exact match to 'Portrait' to distinguish with PortraitUpsideDown */ | ||
| 1140 | boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); | ||
| 1141 | |||
| 1142 | if (contains_Portrait && hint.contains("PortraitUpsideDown")) { | ||
| 1143 | orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; | ||
| 1144 | } else if (contains_Portrait) { | ||
| 1145 | orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; | ||
| 1146 | } else if (hint.contains("PortraitUpsideDown")) { | ||
| 1147 | orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; | ||
| 1148 | } | ||
| 1149 | |||
| 1150 | boolean is_landscape_allowed = (orientation_landscape != -1); | ||
| 1151 | boolean is_portrait_allowed = (orientation_portrait != -1); | ||
| 1152 | int req; /* Requested orientation */ | ||
| 1153 | |||
| 1154 | /* No valid hint, nothing is explicitly allowed */ | ||
| 1155 | if (!is_portrait_allowed && !is_landscape_allowed) { | ||
| 1156 | if (resizable) { | ||
| 1157 | /* All orientations are allowed, respecting user orientation lock setting */ | ||
| 1158 | req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; | ||
| 1159 | } else { | ||
| 1160 | /* Fixed window and nothing specified. Get orientation from w/h of created window */ | ||
| 1161 | req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); | ||
| 1162 | } | ||
| 1163 | } else { | ||
| 1164 | /* At least one orientation is allowed */ | ||
| 1165 | if (resizable) { | ||
| 1166 | if (is_portrait_allowed && is_landscape_allowed) { | ||
| 1167 | /* hint allows both landscape and portrait, promote to full user */ | ||
| 1168 | req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; | ||
| 1169 | } else { | ||
| 1170 | /* Use the only one allowed "orientation" */ | ||
| 1171 | req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); | ||
| 1172 | } | ||
| 1173 | } else { | ||
| 1174 | /* Fixed window and both orientations are allowed. Choose one. */ | ||
| 1175 | if (is_portrait_allowed && is_landscape_allowed) { | ||
| 1176 | req = (w > h ? orientation_landscape : orientation_portrait); | ||
| 1177 | } else { | ||
| 1178 | /* Use the only one allowed "orientation" */ | ||
| 1179 | req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); | ||
| 1180 | } | ||
| 1181 | } | ||
| 1182 | } | ||
| 1183 | |||
| 1184 | Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); | ||
| 1185 | mSingleton.setRequestedOrientation(req); | ||
| 1186 | } | ||
| 1187 | |||
| 1188 | /** | ||
| 1189 | * This method is called by SDL using JNI. | ||
| 1190 | */ | ||
| 1191 | public static void minimizeWindow() { | ||
| 1192 | |||
| 1193 | if (mSingleton == null) { | ||
| 1194 | return; | ||
| 1195 | } | ||
| 1196 | |||
| 1197 | Intent startMain = new Intent(Intent.ACTION_MAIN); | ||
| 1198 | startMain.addCategory(Intent.CATEGORY_HOME); | ||
| 1199 | startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||
| 1200 | mSingleton.startActivity(startMain); | ||
| 1201 | } | ||
| 1202 | |||
| 1203 | /** | ||
| 1204 | * This method is called by SDL using JNI. | ||
| 1205 | */ | ||
| 1206 | public static boolean shouldMinimizeOnFocusLoss() { | ||
| 1207 | return false; | ||
| 1208 | } | ||
| 1209 | |||
| 1210 | /** | ||
| 1211 | * This method is called by SDL using JNI. | ||
| 1212 | */ | ||
| 1213 | public static boolean isScreenKeyboardShown() | ||
| 1214 | { | ||
| 1215 | if (mTextEdit == null) { | ||
| 1216 | return false; | ||
| 1217 | } | ||
| 1218 | |||
| 1219 | if (!mScreenKeyboardShown) { | ||
| 1220 | return false; | ||
| 1221 | } | ||
| 1222 | |||
| 1223 | InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | ||
| 1224 | return imm.isAcceptingText(); | ||
| 1225 | |||
| 1226 | } | ||
| 1227 | |||
| 1228 | /** | ||
| 1229 | * This method is called by SDL using JNI. | ||
| 1230 | */ | ||
| 1231 | public static boolean supportsRelativeMouse() | ||
| 1232 | { | ||
| 1233 | // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under | ||
| 1234 | // Android 7 APIs, and simply returns no data under Android 8 APIs. | ||
| 1235 | // | ||
| 1236 | // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and | ||
| 1237 | // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result, | ||
| 1238 | // we should stick to relative mode. | ||
| 1239 | // | ||
| 1240 | if (Build.VERSION.SDK_INT < 27 /* Android 8.1 (O_MR1) */ && isDeXMode()) { | ||
| 1241 | return false; | ||
| 1242 | } | ||
| 1243 | |||
| 1244 | return SDLActivity.getMotionListener().supportsRelativeMouse(); | ||
| 1245 | } | ||
| 1246 | |||
| 1247 | /** | ||
| 1248 | * This method is called by SDL using JNI. | ||
| 1249 | */ | ||
| 1250 | public static boolean setRelativeMouseEnabled(boolean enabled) | ||
| 1251 | { | ||
| 1252 | if (enabled && !supportsRelativeMouse()) { | ||
| 1253 | return false; | ||
| 1254 | } | ||
| 1255 | |||
| 1256 | return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled); | ||
| 1257 | } | ||
| 1258 | |||
| 1259 | /** | ||
| 1260 | * This method is called by SDL using JNI. | ||
| 1261 | */ | ||
| 1262 | public static boolean sendMessage(int command, int param) { | ||
| 1263 | if (mSingleton == null) { | ||
| 1264 | return false; | ||
| 1265 | } | ||
| 1266 | return mSingleton.sendCommand(command, param); | ||
| 1267 | } | ||
| 1268 | |||
| 1269 | /** | ||
| 1270 | * This method is called by SDL using JNI. | ||
| 1271 | */ | ||
| 1272 | public static Context getContext() { | ||
| 1273 | return SDL.getContext(); | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | /** | ||
| 1277 | * This method is called by SDL using JNI. | ||
| 1278 | */ | ||
| 1279 | public static boolean isAndroidTV() { | ||
| 1280 | UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE); | ||
| 1281 | if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { | ||
| 1282 | return true; | ||
| 1283 | } | ||
| 1284 | if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { | ||
| 1285 | return true; | ||
| 1286 | } | ||
| 1287 | if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { | ||
| 1288 | return true; | ||
| 1289 | } | ||
| 1290 | if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV")) { | ||
| 1291 | return true; | ||
| 1292 | } | ||
| 1293 | return false; | ||
| 1294 | } | ||
| 1295 | |||
| 1296 | public static boolean isVRHeadset() { | ||
| 1297 | if (Build.MANUFACTURER.equals("Oculus") && Build.MODEL.startsWith("Quest")) { | ||
| 1298 | return true; | ||
| 1299 | } | ||
| 1300 | if (Build.MANUFACTURER.equals("Pico")) { | ||
| 1301 | return true; | ||
| 1302 | } | ||
| 1303 | return false; | ||
| 1304 | } | ||
| 1305 | |||
| 1306 | public static double getDiagonal() | ||
| 1307 | { | ||
| 1308 | DisplayMetrics metrics = new DisplayMetrics(); | ||
| 1309 | Activity activity = (Activity)getContext(); | ||
| 1310 | if (activity == null) { | ||
| 1311 | return 0.0; | ||
| 1312 | } | ||
| 1313 | activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); | ||
| 1314 | |||
| 1315 | double dWidthInches = metrics.widthPixels / (double)metrics.xdpi; | ||
| 1316 | double dHeightInches = metrics.heightPixels / (double)metrics.ydpi; | ||
| 1317 | |||
| 1318 | return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); | ||
| 1319 | } | ||
| 1320 | |||
| 1321 | /** | ||
| 1322 | * This method is called by SDL using JNI. | ||
| 1323 | */ | ||
| 1324 | public static boolean isTablet() { | ||
| 1325 | // If our diagonal size is seven inches or greater, we consider ourselves a tablet. | ||
| 1326 | return (getDiagonal() >= 7.0); | ||
| 1327 | } | ||
| 1328 | |||
| 1329 | /** | ||
| 1330 | * This method is called by SDL using JNI. | ||
| 1331 | */ | ||
| 1332 | public static boolean isChromebook() { | ||
| 1333 | if (getContext() == null) { | ||
| 1334 | return false; | ||
| 1335 | } | ||
| 1336 | return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); | ||
| 1337 | } | ||
| 1338 | |||
| 1339 | /** | ||
| 1340 | * This method is called by SDL using JNI. | ||
| 1341 | */ | ||
| 1342 | public static boolean isDeXMode() { | ||
| 1343 | if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { | ||
| 1344 | return false; | ||
| 1345 | } | ||
| 1346 | try { | ||
| 1347 | final Configuration config = getContext().getResources().getConfiguration(); | ||
| 1348 | final Class<?> configClass = config.getClass(); | ||
| 1349 | return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) | ||
| 1350 | == configClass.getField("semDesktopModeEnabled").getInt(config); | ||
| 1351 | } catch(Exception ignored) { | ||
| 1352 | return false; | ||
| 1353 | } | ||
| 1354 | } | ||
| 1355 | |||
| 1356 | /** | ||
| 1357 | * This method is called by SDL using JNI. | ||
| 1358 | */ | ||
| 1359 | public static boolean getManifestEnvironmentVariables() { | ||
| 1360 | try { | ||
| 1361 | if (getContext() == null) { | ||
| 1362 | return false; | ||
| 1363 | } | ||
| 1364 | |||
| 1365 | ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); | ||
| 1366 | Bundle bundle = applicationInfo.metaData; | ||
| 1367 | if (bundle == null) { | ||
| 1368 | return false; | ||
| 1369 | } | ||
| 1370 | String prefix = "SDL_ENV."; | ||
| 1371 | final int trimLength = prefix.length(); | ||
| 1372 | for (String key : bundle.keySet()) { | ||
| 1373 | if (key.startsWith(prefix)) { | ||
| 1374 | String name = key.substring(trimLength); | ||
| 1375 | String value = bundle.get(key).toString(); | ||
| 1376 | nativeSetenv(name, value); | ||
| 1377 | } | ||
| 1378 | } | ||
| 1379 | /* environment variables set! */ | ||
| 1380 | return true; | ||
| 1381 | } catch (Exception e) { | ||
| 1382 | Log.v(TAG, "exception " + e.toString()); | ||
| 1383 | } | ||
| 1384 | return false; | ||
| 1385 | } | ||
| 1386 | |||
| 1387 | // This method is called by SDLControllerManager's API 26 Generic Motion Handler. | ||
| 1388 | public static View getContentView() { | ||
| 1389 | return mLayout; | ||
| 1390 | } | ||
| 1391 | |||
| 1392 | static class ShowTextInputTask implements Runnable { | ||
| 1393 | /* | ||
| 1394 | * This is used to regulate the pan&scan method to have some offset from | ||
| 1395 | * the bottom edge of the input region and the top edge of an input | ||
| 1396 | * method (soft keyboard) | ||
| 1397 | */ | ||
| 1398 | static final int HEIGHT_PADDING = 15; | ||
| 1399 | |||
| 1400 | public int input_type; | ||
| 1401 | public int x, y, w, h; | ||
| 1402 | |||
| 1403 | public ShowTextInputTask(int input_type, int x, int y, int w, int h) { | ||
| 1404 | this.input_type = input_type; | ||
| 1405 | this.x = x; | ||
| 1406 | this.y = y; | ||
| 1407 | this.w = w; | ||
| 1408 | this.h = h; | ||
| 1409 | |||
| 1410 | /* Minimum size of 1 pixel, so it takes focus. */ | ||
| 1411 | if (this.w <= 0) { | ||
| 1412 | this.w = 1; | ||
| 1413 | } | ||
| 1414 | if (this.h + HEIGHT_PADDING <= 0) { | ||
| 1415 | this.h = 1 - HEIGHT_PADDING; | ||
| 1416 | } | ||
| 1417 | } | ||
| 1418 | |||
| 1419 | @Override | ||
| 1420 | public void run() { | ||
| 1421 | RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); | ||
| 1422 | params.leftMargin = x; | ||
| 1423 | params.topMargin = y; | ||
| 1424 | |||
| 1425 | if (mTextEdit == null) { | ||
| 1426 | mTextEdit = new SDLDummyEdit(SDL.getContext()); | ||
| 1427 | |||
| 1428 | mLayout.addView(mTextEdit, params); | ||
| 1429 | } else { | ||
| 1430 | mTextEdit.setLayoutParams(params); | ||
| 1431 | } | ||
| 1432 | mTextEdit.setInputType(input_type); | ||
| 1433 | |||
| 1434 | mTextEdit.setVisibility(View.VISIBLE); | ||
| 1435 | mTextEdit.requestFocus(); | ||
| 1436 | |||
| 1437 | InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | ||
| 1438 | imm.showSoftInput(mTextEdit, 0); | ||
| 1439 | |||
| 1440 | mScreenKeyboardShown = true; | ||
| 1441 | } | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | /** | ||
| 1445 | * This method is called by SDL using JNI. | ||
| 1446 | */ | ||
| 1447 | public static boolean showTextInput(int input_type, int x, int y, int w, int h) { | ||
| 1448 | // Transfer the task to the main thread as a Runnable | ||
| 1449 | return mSingleton.commandHandler.post(new ShowTextInputTask(input_type, x, y, w, h)); | ||
| 1450 | } | ||
| 1451 | |||
| 1452 | public static boolean isTextInputEvent(KeyEvent event) { | ||
| 1453 | |||
| 1454 | // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT | ||
| 1455 | if (event.isCtrlPressed()) { | ||
| 1456 | return false; | ||
| 1457 | } | ||
| 1458 | |||
| 1459 | return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; | ||
| 1460 | } | ||
| 1461 | |||
| 1462 | public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) { | ||
| 1463 | int deviceId = event.getDeviceId(); | ||
| 1464 | int source = event.getSource(); | ||
| 1465 | |||
| 1466 | if (source == InputDevice.SOURCE_UNKNOWN) { | ||
| 1467 | InputDevice device = InputDevice.getDevice(deviceId); | ||
| 1468 | if (device != null) { | ||
| 1469 | source = device.getSources(); | ||
| 1470 | } | ||
| 1471 | } | ||
| 1472 | |||
| 1473 | // if (event.getAction() == KeyEvent.ACTION_DOWN) { | ||
| 1474 | // Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); | ||
| 1475 | // } else if (event.getAction() == KeyEvent.ACTION_UP) { | ||
| 1476 | // Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); | ||
| 1477 | // } | ||
| 1478 | |||
| 1479 | // Dispatch the different events depending on where they come from | ||
| 1480 | // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD | ||
| 1481 | // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD | ||
| 1482 | // | ||
| 1483 | // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and | ||
| 1484 | // SOURCE_JOYSTICK, while its key events arrive from the keyboard source | ||
| 1485 | // So, retrieve the device itself and check all of its sources | ||
| 1486 | if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { | ||
| 1487 | // Note that we process events with specific key codes here | ||
| 1488 | if (event.getAction() == KeyEvent.ACTION_DOWN) { | ||
| 1489 | if (SDLControllerManager.onNativePadDown(deviceId, keyCode)) { | ||
| 1490 | return true; | ||
| 1491 | } | ||
| 1492 | } else if (event.getAction() == KeyEvent.ACTION_UP) { | ||
| 1493 | if (SDLControllerManager.onNativePadUp(deviceId, keyCode)) { | ||
| 1494 | return true; | ||
| 1495 | } | ||
| 1496 | } | ||
| 1497 | } | ||
| 1498 | |||
| 1499 | if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { | ||
| 1500 | if (SDLActivity.isVRHeadset()) { | ||
| 1501 | // The Oculus Quest controller back button comes in as source mouse, so accept that | ||
| 1502 | } else { | ||
| 1503 | // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses | ||
| 1504 | // they are ignored here because sending them as mouse input to SDL is messy | ||
| 1505 | if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { | ||
| 1506 | switch (event.getAction()) { | ||
| 1507 | case KeyEvent.ACTION_DOWN: | ||
| 1508 | case KeyEvent.ACTION_UP: | ||
| 1509 | // mark the event as handled or it will be handled by system | ||
| 1510 | // handling KEYCODE_BACK by system will call onBackPressed() | ||
| 1511 | return true; | ||
| 1512 | } | ||
| 1513 | } | ||
| 1514 | } | ||
| 1515 | } | ||
| 1516 | |||
| 1517 | if (event.getAction() == KeyEvent.ACTION_DOWN) { | ||
| 1518 | onNativeKeyDown(keyCode); | ||
| 1519 | |||
| 1520 | if (isTextInputEvent(event)) { | ||
| 1521 | if (ic != null) { | ||
| 1522 | ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); | ||
| 1523 | } else { | ||
| 1524 | SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); | ||
| 1525 | } | ||
| 1526 | } | ||
| 1527 | return true; | ||
| 1528 | } else if (event.getAction() == KeyEvent.ACTION_UP) { | ||
| 1529 | onNativeKeyUp(keyCode); | ||
| 1530 | return true; | ||
| 1531 | } | ||
| 1532 | |||
| 1533 | return false; | ||
| 1534 | } | ||
| 1535 | |||
| 1536 | /** | ||
| 1537 | * This method is called by SDL using JNI. | ||
| 1538 | */ | ||
| 1539 | public static Surface getNativeSurface() { | ||
| 1540 | if (SDLActivity.mSurface == null) { | ||
| 1541 | return null; | ||
| 1542 | } | ||
| 1543 | return SDLActivity.mSurface.getNativeSurface(); | ||
| 1544 | } | ||
| 1545 | |||
| 1546 | // Input | ||
| 1547 | |||
| 1548 | /** | ||
| 1549 | * This method is called by SDL using JNI. | ||
| 1550 | */ | ||
| 1551 | public static void initTouch() { | ||
| 1552 | int[] ids = InputDevice.getDeviceIds(); | ||
| 1553 | |||
| 1554 | for (int id : ids) { | ||
| 1555 | InputDevice device = InputDevice.getDevice(id); | ||
| 1556 | /* Allow SOURCE_TOUCHSCREEN and also Virtual InputDevices because they can send TOUCHSCREEN events */ | ||
| 1557 | if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN | ||
| 1558 | || device.isVirtual())) { | ||
| 1559 | |||
| 1560 | nativeAddTouch(device.getId(), device.getName()); | ||
| 1561 | } | ||
| 1562 | } | ||
| 1563 | } | ||
| 1564 | |||
| 1565 | // Messagebox | ||
| 1566 | |||
| 1567 | /** Result of current messagebox. Also used for blocking the calling thread. */ | ||
| 1568 | protected final int[] messageboxSelection = new int[1]; | ||
| 1569 | |||
| 1570 | /** | ||
| 1571 | * This method is called by SDL using JNI. | ||
| 1572 | * Shows the messagebox from UI thread and block calling thread. | ||
| 1573 | * buttonFlags, buttonIds and buttonTexts must have same length. | ||
| 1574 | * @param buttonFlags array containing flags for every button. | ||
| 1575 | * @param buttonIds array containing id for every button. | ||
| 1576 | * @param buttonTexts array containing text for every button. | ||
| 1577 | * @param colors null for default or array of length 5 containing colors. | ||
| 1578 | * @return button id or -1. | ||
| 1579 | */ | ||
| 1580 | public int messageboxShowMessageBox( | ||
| 1581 | final int flags, | ||
| 1582 | final String title, | ||
| 1583 | final String message, | ||
| 1584 | final int[] buttonFlags, | ||
| 1585 | final int[] buttonIds, | ||
| 1586 | final String[] buttonTexts, | ||
| 1587 | final int[] colors) { | ||
| 1588 | |||
| 1589 | messageboxSelection[0] = -1; | ||
| 1590 | |||
| 1591 | // sanity checks | ||
| 1592 | |||
| 1593 | if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { | ||
| 1594 | return -1; // implementation broken | ||
| 1595 | } | ||
| 1596 | |||
| 1597 | // collect arguments for Dialog | ||
| 1598 | |||
| 1599 | final Bundle args = new Bundle(); | ||
| 1600 | args.putInt("flags", flags); | ||
| 1601 | args.putString("title", title); | ||
| 1602 | args.putString("message", message); | ||
| 1603 | args.putIntArray("buttonFlags", buttonFlags); | ||
| 1604 | args.putIntArray("buttonIds", buttonIds); | ||
| 1605 | args.putStringArray("buttonTexts", buttonTexts); | ||
| 1606 | args.putIntArray("colors", colors); | ||
| 1607 | |||
| 1608 | // trigger Dialog creation on UI thread | ||
| 1609 | |||
| 1610 | runOnUiThread(new Runnable() { | ||
| 1611 | @Override | ||
| 1612 | public void run() { | ||
| 1613 | messageboxCreateAndShow(args); | ||
| 1614 | } | ||
| 1615 | }); | ||
| 1616 | |||
| 1617 | // block the calling thread | ||
| 1618 | |||
| 1619 | synchronized (messageboxSelection) { | ||
| 1620 | try { | ||
| 1621 | messageboxSelection.wait(); | ||
| 1622 | } catch (InterruptedException ex) { | ||
| 1623 | ex.printStackTrace(); | ||
| 1624 | return -1; | ||
| 1625 | } | ||
| 1626 | } | ||
| 1627 | |||
| 1628 | // return selected value | ||
| 1629 | |||
| 1630 | return messageboxSelection[0]; | ||
| 1631 | } | ||
| 1632 | |||
| 1633 | protected void messageboxCreateAndShow(Bundle args) { | ||
| 1634 | |||
| 1635 | // TODO set values from "flags" to messagebox dialog | ||
| 1636 | |||
| 1637 | // get colors | ||
| 1638 | |||
| 1639 | int[] colors = args.getIntArray("colors"); | ||
| 1640 | int backgroundColor; | ||
| 1641 | int textColor; | ||
| 1642 | int buttonBorderColor; | ||
| 1643 | int buttonBackgroundColor; | ||
| 1644 | int buttonSelectedColor; | ||
| 1645 | if (colors != null) { | ||
| 1646 | int i = -1; | ||
| 1647 | backgroundColor = colors[++i]; | ||
| 1648 | textColor = colors[++i]; | ||
| 1649 | buttonBorderColor = colors[++i]; | ||
| 1650 | buttonBackgroundColor = colors[++i]; | ||
| 1651 | buttonSelectedColor = colors[++i]; | ||
| 1652 | } else { | ||
| 1653 | backgroundColor = Color.TRANSPARENT; | ||
| 1654 | textColor = Color.TRANSPARENT; | ||
| 1655 | buttonBorderColor = Color.TRANSPARENT; | ||
| 1656 | buttonBackgroundColor = Color.TRANSPARENT; | ||
| 1657 | buttonSelectedColor = Color.TRANSPARENT; | ||
| 1658 | } | ||
| 1659 | |||
| 1660 | // create dialog with title and a listener to wake up calling thread | ||
| 1661 | |||
| 1662 | final AlertDialog dialog = new AlertDialog.Builder(this).create(); | ||
| 1663 | dialog.setTitle(args.getString("title")); | ||
| 1664 | dialog.setCancelable(false); | ||
| 1665 | dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { | ||
| 1666 | @Override | ||
| 1667 | public void onDismiss(DialogInterface unused) { | ||
| 1668 | synchronized (messageboxSelection) { | ||
| 1669 | messageboxSelection.notify(); | ||
| 1670 | } | ||
| 1671 | } | ||
| 1672 | }); | ||
| 1673 | |||
| 1674 | // create text | ||
| 1675 | |||
| 1676 | TextView message = new TextView(this); | ||
| 1677 | message.setGravity(Gravity.CENTER); | ||
| 1678 | message.setText(args.getString("message")); | ||
| 1679 | if (textColor != Color.TRANSPARENT) { | ||
| 1680 | message.setTextColor(textColor); | ||
| 1681 | } | ||
| 1682 | |||
| 1683 | // create buttons | ||
| 1684 | |||
| 1685 | int[] buttonFlags = args.getIntArray("buttonFlags"); | ||
| 1686 | int[] buttonIds = args.getIntArray("buttonIds"); | ||
| 1687 | String[] buttonTexts = args.getStringArray("buttonTexts"); | ||
| 1688 | |||
| 1689 | final SparseArray<Button> mapping = new SparseArray<Button>(); | ||
| 1690 | |||
| 1691 | LinearLayout buttons = new LinearLayout(this); | ||
| 1692 | buttons.setOrientation(LinearLayout.HORIZONTAL); | ||
| 1693 | buttons.setGravity(Gravity.CENTER); | ||
| 1694 | for (int i = 0; i < buttonTexts.length; ++i) { | ||
| 1695 | Button button = new Button(this); | ||
| 1696 | final int id = buttonIds[i]; | ||
| 1697 | button.setOnClickListener(new View.OnClickListener() { | ||
| 1698 | @Override | ||
| 1699 | public void onClick(View v) { | ||
| 1700 | messageboxSelection[0] = id; | ||
| 1701 | dialog.dismiss(); | ||
| 1702 | } | ||
| 1703 | }); | ||
| 1704 | if (buttonFlags[i] != 0) { | ||
| 1705 | // see SDL_messagebox.h | ||
| 1706 | if ((buttonFlags[i] & 0x00000001) != 0) { | ||
| 1707 | mapping.put(KeyEvent.KEYCODE_ENTER, button); | ||
| 1708 | } | ||
| 1709 | if ((buttonFlags[i] & 0x00000002) != 0) { | ||
| 1710 | mapping.put(KeyEvent.KEYCODE_ESCAPE, button); /* API 11 */ | ||
| 1711 | } | ||
| 1712 | } | ||
| 1713 | button.setText(buttonTexts[i]); | ||
| 1714 | if (textColor != Color.TRANSPARENT) { | ||
| 1715 | button.setTextColor(textColor); | ||
| 1716 | } | ||
| 1717 | if (buttonBorderColor != Color.TRANSPARENT) { | ||
| 1718 | // TODO set color for border of messagebox button | ||
| 1719 | } | ||
| 1720 | if (buttonBackgroundColor != Color.TRANSPARENT) { | ||
| 1721 | Drawable drawable = button.getBackground(); | ||
| 1722 | if (drawable == null) { | ||
| 1723 | // setting the color this way removes the style | ||
| 1724 | button.setBackgroundColor(buttonBackgroundColor); | ||
| 1725 | } else { | ||
| 1726 | // setting the color this way keeps the style (gradient, padding, etc.) | ||
| 1727 | drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY); | ||
| 1728 | } | ||
| 1729 | } | ||
| 1730 | if (buttonSelectedColor != Color.TRANSPARENT) { | ||
| 1731 | // TODO set color for selected messagebox button | ||
| 1732 | } | ||
| 1733 | buttons.addView(button); | ||
| 1734 | } | ||
| 1735 | |||
| 1736 | // create content | ||
| 1737 | |||
| 1738 | LinearLayout content = new LinearLayout(this); | ||
| 1739 | content.setOrientation(LinearLayout.VERTICAL); | ||
| 1740 | content.addView(message); | ||
| 1741 | content.addView(buttons); | ||
| 1742 | if (backgroundColor != Color.TRANSPARENT) { | ||
| 1743 | content.setBackgroundColor(backgroundColor); | ||
| 1744 | } | ||
| 1745 | |||
| 1746 | // add content to dialog and return | ||
| 1747 | |||
| 1748 | dialog.setView(content); | ||
| 1749 | dialog.setOnKeyListener(new Dialog.OnKeyListener() { | ||
| 1750 | @Override | ||
| 1751 | public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) { | ||
| 1752 | Button button = mapping.get(keyCode); | ||
| 1753 | if (button != null) { | ||
| 1754 | if (event.getAction() == KeyEvent.ACTION_UP) { | ||
| 1755 | button.performClick(); | ||
| 1756 | } | ||
| 1757 | return true; // also for ignored actions | ||
| 1758 | } | ||
| 1759 | return false; | ||
| 1760 | } | ||
| 1761 | }); | ||
| 1762 | |||
| 1763 | dialog.show(); | ||
| 1764 | } | ||
| 1765 | |||
| 1766 | private final Runnable rehideSystemUi = new Runnable() { | ||
| 1767 | @Override | ||
| 1768 | public void run() { | ||
| 1769 | if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { | ||
| 1770 | int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | | ||
| 1771 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | | ||
| 1772 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | | ||
| 1773 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | | ||
| 1774 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | | ||
| 1775 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; | ||
| 1776 | |||
| 1777 | SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags); | ||
| 1778 | } | ||
| 1779 | } | ||
| 1780 | }; | ||
| 1781 | |||
| 1782 | public void onSystemUiVisibilityChange(int visibility) { | ||
| 1783 | if (SDLActivity.mFullscreenModeActive && ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)) { | ||
| 1784 | |||
| 1785 | Handler handler = getWindow().getDecorView().getHandler(); | ||
| 1786 | if (handler != null) { | ||
| 1787 | handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop. | ||
| 1788 | handler.postDelayed(rehideSystemUi, 2000); | ||
| 1789 | } | ||
| 1790 | |||
| 1791 | } | ||
| 1792 | } | ||
| 1793 | |||
| 1794 | /** | ||
| 1795 | * This method is called by SDL using JNI. | ||
| 1796 | */ | ||
| 1797 | public static boolean clipboardHasText() { | ||
| 1798 | return mClipboardHandler.clipboardHasText(); | ||
| 1799 | } | ||
| 1800 | |||
| 1801 | /** | ||
| 1802 | * This method is called by SDL using JNI. | ||
| 1803 | */ | ||
| 1804 | public static String clipboardGetText() { | ||
| 1805 | return mClipboardHandler.clipboardGetText(); | ||
| 1806 | } | ||
| 1807 | |||
| 1808 | /** | ||
| 1809 | * This method is called by SDL using JNI. | ||
| 1810 | */ | ||
| 1811 | public static void clipboardSetText(String string) { | ||
| 1812 | mClipboardHandler.clipboardSetText(string); | ||
| 1813 | } | ||
| 1814 | |||
| 1815 | /** | ||
| 1816 | * This method is called by SDL using JNI. | ||
| 1817 | */ | ||
| 1818 | public static int createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY) { | ||
| 1819 | Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); | ||
| 1820 | ++mLastCursorID; | ||
| 1821 | |||
| 1822 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 1823 | try { | ||
| 1824 | mCursors.put(mLastCursorID, PointerIcon.create(bitmap, hotSpotX, hotSpotY)); | ||
| 1825 | } catch (Exception e) { | ||
| 1826 | return 0; | ||
| 1827 | } | ||
| 1828 | } else { | ||
| 1829 | return 0; | ||
| 1830 | } | ||
| 1831 | return mLastCursorID; | ||
| 1832 | } | ||
| 1833 | |||
| 1834 | /** | ||
| 1835 | * This method is called by SDL using JNI. | ||
| 1836 | */ | ||
| 1837 | public static void destroyCustomCursor(int cursorID) { | ||
| 1838 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 1839 | try { | ||
| 1840 | mCursors.remove(cursorID); | ||
| 1841 | } catch (Exception e) { | ||
| 1842 | } | ||
| 1843 | } | ||
| 1844 | return; | ||
| 1845 | } | ||
| 1846 | |||
| 1847 | /** | ||
| 1848 | * This method is called by SDL using JNI. | ||
| 1849 | */ | ||
| 1850 | public static boolean setCustomCursor(int cursorID) { | ||
| 1851 | |||
| 1852 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 1853 | try { | ||
| 1854 | mSurface.setPointerIcon(mCursors.get(cursorID)); | ||
| 1855 | } catch (Exception e) { | ||
| 1856 | return false; | ||
| 1857 | } | ||
| 1858 | } else { | ||
| 1859 | return false; | ||
| 1860 | } | ||
| 1861 | return true; | ||
| 1862 | } | ||
| 1863 | |||
| 1864 | /** | ||
| 1865 | * This method is called by SDL using JNI. | ||
| 1866 | */ | ||
| 1867 | public static boolean setSystemCursor(int cursorID) { | ||
| 1868 | int cursor_type = 0; //PointerIcon.TYPE_NULL; | ||
| 1869 | switch (cursorID) { | ||
| 1870 | case SDL_SYSTEM_CURSOR_ARROW: | ||
| 1871 | cursor_type = 1000; //PointerIcon.TYPE_ARROW; | ||
| 1872 | break; | ||
| 1873 | case SDL_SYSTEM_CURSOR_IBEAM: | ||
| 1874 | cursor_type = 1008; //PointerIcon.TYPE_TEXT; | ||
| 1875 | break; | ||
| 1876 | case SDL_SYSTEM_CURSOR_WAIT: | ||
| 1877 | cursor_type = 1004; //PointerIcon.TYPE_WAIT; | ||
| 1878 | break; | ||
| 1879 | case SDL_SYSTEM_CURSOR_CROSSHAIR: | ||
| 1880 | cursor_type = 1007; //PointerIcon.TYPE_CROSSHAIR; | ||
| 1881 | break; | ||
| 1882 | case SDL_SYSTEM_CURSOR_WAITARROW: | ||
| 1883 | cursor_type = 1004; //PointerIcon.TYPE_WAIT; | ||
| 1884 | break; | ||
| 1885 | case SDL_SYSTEM_CURSOR_SIZENWSE: | ||
| 1886 | cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; | ||
| 1887 | break; | ||
| 1888 | case SDL_SYSTEM_CURSOR_SIZENESW: | ||
| 1889 | cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; | ||
| 1890 | break; | ||
| 1891 | case SDL_SYSTEM_CURSOR_SIZEWE: | ||
| 1892 | cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; | ||
| 1893 | break; | ||
| 1894 | case SDL_SYSTEM_CURSOR_SIZENS: | ||
| 1895 | cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; | ||
| 1896 | break; | ||
| 1897 | case SDL_SYSTEM_CURSOR_SIZEALL: | ||
| 1898 | cursor_type = 1020; //PointerIcon.TYPE_GRAB; | ||
| 1899 | break; | ||
| 1900 | case SDL_SYSTEM_CURSOR_NO: | ||
| 1901 | cursor_type = 1012; //PointerIcon.TYPE_NO_DROP; | ||
| 1902 | break; | ||
| 1903 | case SDL_SYSTEM_CURSOR_HAND: | ||
| 1904 | cursor_type = 1002; //PointerIcon.TYPE_HAND; | ||
| 1905 | break; | ||
| 1906 | case SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT: | ||
| 1907 | cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; | ||
| 1908 | break; | ||
| 1909 | case SDL_SYSTEM_CURSOR_WINDOW_TOP: | ||
| 1910 | cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; | ||
| 1911 | break; | ||
| 1912 | case SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT: | ||
| 1913 | cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; | ||
| 1914 | break; | ||
| 1915 | case SDL_SYSTEM_CURSOR_WINDOW_RIGHT: | ||
| 1916 | cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; | ||
| 1917 | break; | ||
| 1918 | case SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT: | ||
| 1919 | cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; | ||
| 1920 | break; | ||
| 1921 | case SDL_SYSTEM_CURSOR_WINDOW_BOTTOM: | ||
| 1922 | cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; | ||
| 1923 | break; | ||
| 1924 | case SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT: | ||
| 1925 | cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; | ||
| 1926 | break; | ||
| 1927 | case SDL_SYSTEM_CURSOR_WINDOW_LEFT: | ||
| 1928 | cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; | ||
| 1929 | break; | ||
| 1930 | } | ||
| 1931 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 1932 | try { | ||
| 1933 | mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type)); | ||
| 1934 | } catch (Exception e) { | ||
| 1935 | return false; | ||
| 1936 | } | ||
| 1937 | } | ||
| 1938 | return true; | ||
| 1939 | } | ||
| 1940 | |||
| 1941 | /** | ||
| 1942 | * This method is called by SDL using JNI. | ||
| 1943 | */ | ||
| 1944 | public static void requestPermission(String permission, int requestCode) { | ||
| 1945 | if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { | ||
| 1946 | nativePermissionResult(requestCode, true); | ||
| 1947 | return; | ||
| 1948 | } | ||
| 1949 | |||
| 1950 | Activity activity = (Activity)getContext(); | ||
| 1951 | if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { | ||
| 1952 | activity.requestPermissions(new String[]{permission}, requestCode); | ||
| 1953 | } else { | ||
| 1954 | nativePermissionResult(requestCode, true); | ||
| 1955 | } | ||
| 1956 | } | ||
| 1957 | |||
| 1958 | @Override | ||
| 1959 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { | ||
| 1960 | boolean result = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED); | ||
| 1961 | nativePermissionResult(requestCode, result); | ||
| 1962 | } | ||
| 1963 | |||
| 1964 | /** | ||
| 1965 | * This method is called by SDL using JNI. | ||
| 1966 | */ | ||
| 1967 | public static boolean openURL(String url) | ||
| 1968 | { | ||
| 1969 | try { | ||
| 1970 | Intent i = new Intent(Intent.ACTION_VIEW); | ||
| 1971 | i.setData(Uri.parse(url)); | ||
| 1972 | |||
| 1973 | int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; | ||
| 1974 | if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { | ||
| 1975 | flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT; | ||
| 1976 | } else { | ||
| 1977 | flags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET; | ||
| 1978 | } | ||
| 1979 | i.addFlags(flags); | ||
| 1980 | |||
| 1981 | mSingleton.startActivity(i); | ||
| 1982 | } catch (Exception ex) { | ||
| 1983 | return false; | ||
| 1984 | } | ||
| 1985 | return true; | ||
| 1986 | } | ||
| 1987 | |||
| 1988 | /** | ||
| 1989 | * This method is called by SDL using JNI. | ||
| 1990 | */ | ||
| 1991 | public static boolean showToast(String message, int duration, int gravity, int xOffset, int yOffset) | ||
| 1992 | { | ||
| 1993 | if(null == mSingleton) { | ||
| 1994 | return false; | ||
| 1995 | } | ||
| 1996 | |||
| 1997 | try | ||
| 1998 | { | ||
| 1999 | class OneShotTask implements Runnable { | ||
| 2000 | private final String mMessage; | ||
| 2001 | private final int mDuration; | ||
| 2002 | private final int mGravity; | ||
| 2003 | private final int mXOffset; | ||
| 2004 | private final int mYOffset; | ||
| 2005 | |||
| 2006 | OneShotTask(String message, int duration, int gravity, int xOffset, int yOffset) { | ||
| 2007 | mMessage = message; | ||
| 2008 | mDuration = duration; | ||
| 2009 | mGravity = gravity; | ||
| 2010 | mXOffset = xOffset; | ||
| 2011 | mYOffset = yOffset; | ||
| 2012 | } | ||
| 2013 | |||
| 2014 | public void run() { | ||
| 2015 | try | ||
| 2016 | { | ||
| 2017 | Toast toast = Toast.makeText(mSingleton, mMessage, mDuration); | ||
| 2018 | if (mGravity >= 0) { | ||
| 2019 | toast.setGravity(mGravity, mXOffset, mYOffset); | ||
| 2020 | } | ||
| 2021 | toast.show(); | ||
| 2022 | } catch(Exception ex) { | ||
| 2023 | Log.e(TAG, ex.getMessage()); | ||
| 2024 | } | ||
| 2025 | } | ||
| 2026 | } | ||
| 2027 | mSingleton.runOnUiThread(new OneShotTask(message, duration, gravity, xOffset, yOffset)); | ||
| 2028 | } catch(Exception ex) { | ||
| 2029 | return false; | ||
| 2030 | } | ||
| 2031 | return true; | ||
| 2032 | } | ||
| 2033 | |||
| 2034 | /** | ||
| 2035 | * This method is called by SDL using JNI. | ||
| 2036 | */ | ||
| 2037 | public static int openFileDescriptor(String uri, String mode) throws Exception { | ||
| 2038 | if (mSingleton == null) { | ||
| 2039 | return -1; | ||
| 2040 | } | ||
| 2041 | |||
| 2042 | try { | ||
| 2043 | ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode); | ||
| 2044 | return pfd != null ? pfd.detachFd() : -1; | ||
| 2045 | } catch (FileNotFoundException e) { | ||
| 2046 | e.printStackTrace(); | ||
| 2047 | return -1; | ||
| 2048 | } | ||
| 2049 | } | ||
| 2050 | |||
| 2051 | /** | ||
| 2052 | * This method is called by SDL using JNI. | ||
| 2053 | */ | ||
| 2054 | public static boolean showFileDialog(String[] filters, boolean allowMultiple, boolean forWrite, int requestCode) { | ||
| 2055 | if (mSingleton == null) { | ||
| 2056 | return false; | ||
| 2057 | } | ||
| 2058 | |||
| 2059 | if (forWrite) { | ||
| 2060 | allowMultiple = false; | ||
| 2061 | } | ||
| 2062 | |||
| 2063 | /* Convert string list of extensions to their respective MIME types */ | ||
| 2064 | ArrayList<String> mimes = new ArrayList<>(); | ||
| 2065 | MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); | ||
| 2066 | if (filters != null) { | ||
| 2067 | for (String pattern : filters) { | ||
| 2068 | String[] extensions = pattern.split(";"); | ||
| 2069 | |||
| 2070 | if (extensions.length == 1 && extensions[0].equals("*")) { | ||
| 2071 | /* Handle "*" special case */ | ||
| 2072 | mimes.add("*/*"); | ||
| 2073 | } else { | ||
| 2074 | for (String ext : extensions) { | ||
| 2075 | String mime = mimeTypeMap.getMimeTypeFromExtension(ext); | ||
| 2076 | if (mime != null) { | ||
| 2077 | mimes.add(mime); | ||
| 2078 | } | ||
| 2079 | } | ||
| 2080 | } | ||
| 2081 | } | ||
| 2082 | } | ||
| 2083 | |||
| 2084 | /* Display the file dialog */ | ||
| 2085 | Intent intent = new Intent(forWrite ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_OPEN_DOCUMENT); | ||
| 2086 | intent.addCategory(Intent.CATEGORY_OPENABLE); | ||
| 2087 | intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); | ||
| 2088 | switch (mimes.size()) { | ||
| 2089 | case 0: | ||
| 2090 | intent.setType("*/*"); | ||
| 2091 | break; | ||
| 2092 | case 1: | ||
| 2093 | intent.setType(mimes.get(0)); | ||
| 2094 | break; | ||
| 2095 | default: | ||
| 2096 | intent.setType("*/*"); | ||
| 2097 | intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.toArray(new String[]{})); | ||
| 2098 | } | ||
| 2099 | |||
| 2100 | try { | ||
| 2101 | mSingleton.startActivityForResult(intent, requestCode); | ||
| 2102 | } catch (ActivityNotFoundException e) { | ||
| 2103 | Log.e(TAG, "Unable to open file dialog.", e); | ||
| 2104 | return false; | ||
| 2105 | } | ||
| 2106 | |||
| 2107 | /* Save current dialog state */ | ||
| 2108 | mFileDialogState = new SDLFileDialogState(); | ||
| 2109 | mFileDialogState.requestCode = requestCode; | ||
| 2110 | mFileDialogState.multipleChoice = allowMultiple; | ||
| 2111 | return true; | ||
| 2112 | } | ||
| 2113 | |||
| 2114 | /* Internal class used to track active open file dialog */ | ||
| 2115 | static class SDLFileDialogState { | ||
| 2116 | int requestCode; | ||
| 2117 | boolean multipleChoice; | ||
| 2118 | } | ||
| 2119 | } | ||
| 2120 | |||
| 2121 | /** | ||
| 2122 | Simple runnable to start the SDL application | ||
| 2123 | */ | ||
| 2124 | class SDLMain implements Runnable { | ||
| 2125 | @Override | ||
| 2126 | public void run() { | ||
| 2127 | // Runs SDLActivity.main() | ||
| 2128 | |||
| 2129 | try { | ||
| 2130 | android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY); | ||
| 2131 | } catch (Exception e) { | ||
| 2132 | Log.v("SDL", "modify thread properties failed " + e.toString()); | ||
| 2133 | } | ||
| 2134 | |||
| 2135 | SDLActivity.nativeInitMainThread(); | ||
| 2136 | SDLActivity.mSingleton.main(); | ||
| 2137 | SDLActivity.nativeCleanupMainThread(); | ||
| 2138 | |||
| 2139 | if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) { | ||
| 2140 | // Let's finish the Activity | ||
| 2141 | SDLActivity.mSDLThread = null; | ||
| 2142 | SDLActivity.mSDLMainFinished = true; | ||
| 2143 | SDLActivity.mSingleton.finish(); | ||
| 2144 | } // else: Activity is already being destroyed | ||
| 2145 | |||
| 2146 | } | ||
| 2147 | } | ||
| 2148 | |||
| 2149 | class SDLClipboardHandler implements | ||
| 2150 | ClipboardManager.OnPrimaryClipChangedListener { | ||
| 2151 | |||
| 2152 | protected ClipboardManager mClipMgr; | ||
| 2153 | |||
| 2154 | SDLClipboardHandler() { | ||
| 2155 | mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE); | ||
| 2156 | mClipMgr.addPrimaryClipChangedListener(this); | ||
| 2157 | } | ||
| 2158 | |||
| 2159 | public boolean clipboardHasText() { | ||
| 2160 | return mClipMgr.hasPrimaryClip(); | ||
| 2161 | } | ||
| 2162 | |||
| 2163 | public String clipboardGetText() { | ||
| 2164 | ClipData clip = mClipMgr.getPrimaryClip(); | ||
| 2165 | if (clip != null) { | ||
| 2166 | ClipData.Item item = clip.getItemAt(0); | ||
| 2167 | if (item != null) { | ||
| 2168 | CharSequence text = item.getText(); | ||
| 2169 | if (text != null) { | ||
| 2170 | return text.toString(); | ||
| 2171 | } | ||
| 2172 | } | ||
| 2173 | } | ||
| 2174 | return null; | ||
| 2175 | } | ||
| 2176 | |||
| 2177 | public void clipboardSetText(String string) { | ||
| 2178 | mClipMgr.removePrimaryClipChangedListener(this); | ||
| 2179 | ClipData clip = ClipData.newPlainText(null, string); | ||
| 2180 | mClipMgr.setPrimaryClip(clip); | ||
| 2181 | mClipMgr.addPrimaryClipChangedListener(this); | ||
| 2182 | } | ||
| 2183 | |||
| 2184 | @Override | ||
| 2185 | public void onPrimaryClipChanged() { | ||
| 2186 | SDLActivity.onNativeClipboardChanged(); | ||
| 2187 | } | ||
| 2188 | } | ||
| 2189 | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLAudioManager.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLAudioManager.java new file mode 100644 index 0000000..6ad2f54 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLAudioManager.java | |||
| @@ -0,0 +1,126 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.content.Context; | ||
| 4 | import android.media.AudioDeviceCallback; | ||
| 5 | import android.media.AudioDeviceInfo; | ||
| 6 | import android.media.AudioManager; | ||
| 7 | import android.os.Build; | ||
| 8 | import android.util.Log; | ||
| 9 | |||
| 10 | import java.util.Arrays; | ||
| 11 | import java.util.ArrayList; | ||
| 12 | |||
| 13 | public class SDLAudioManager { | ||
| 14 | protected static final String TAG = "SDLAudio"; | ||
| 15 | |||
| 16 | protected static Context mContext; | ||
| 17 | |||
| 18 | private static AudioDeviceCallback mAudioDeviceCallback; | ||
| 19 | |||
| 20 | public static void initialize() { | ||
| 21 | mAudioDeviceCallback = null; | ||
| 22 | |||
| 23 | if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) | ||
| 24 | { | ||
| 25 | mAudioDeviceCallback = new AudioDeviceCallback() { | ||
| 26 | @Override | ||
| 27 | public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { | ||
| 28 | for (AudioDeviceInfo deviceInfo : addedDevices) { | ||
| 29 | addAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId()); | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | @Override | ||
| 34 | public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { | ||
| 35 | for (AudioDeviceInfo deviceInfo : removedDevices) { | ||
| 36 | removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()); | ||
| 37 | } | ||
| 38 | } | ||
| 39 | }; | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | public static void setContext(Context context) { | ||
| 44 | mContext = context; | ||
| 45 | } | ||
| 46 | |||
| 47 | public static void release(Context context) { | ||
| 48 | // no-op atm | ||
| 49 | } | ||
| 50 | |||
| 51 | // Audio | ||
| 52 | |||
| 53 | private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) { | ||
| 54 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 55 | AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); | ||
| 56 | for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) { | ||
| 57 | if (deviceInfo.getId() == deviceId) { | ||
| 58 | return deviceInfo; | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | return null; | ||
| 63 | } | ||
| 64 | |||
| 65 | private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) { | ||
| 66 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 67 | AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); | ||
| 68 | for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { | ||
| 69 | if (deviceInfo.getId() == deviceId) { | ||
| 70 | return deviceInfo; | ||
| 71 | } | ||
| 72 | } | ||
| 73 | } | ||
| 74 | return null; | ||
| 75 | } | ||
| 76 | |||
| 77 | public static void registerAudioDeviceCallback() { | ||
| 78 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 79 | AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); | ||
| 80 | // get an initial list now, before hotplug callbacks fire. | ||
| 81 | for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { | ||
| 82 | if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) { | ||
| 83 | continue; // Device cannot be opened | ||
| 84 | } | ||
| 85 | addAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId()); | ||
| 86 | } | ||
| 87 | for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) { | ||
| 88 | addAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId()); | ||
| 89 | } | ||
| 90 | audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null); | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | public static void unregisterAudioDeviceCallback() { | ||
| 95 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 96 | AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); | ||
| 97 | audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback); | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | /** This method is called by SDL using JNI. */ | ||
| 102 | public static void audioSetThreadPriority(boolean recording, int device_id) { | ||
| 103 | try { | ||
| 104 | |||
| 105 | /* Set thread name */ | ||
| 106 | if (recording) { | ||
| 107 | Thread.currentThread().setName("SDLAudioC" + device_id); | ||
| 108 | } else { | ||
| 109 | Thread.currentThread().setName("SDLAudioP" + device_id); | ||
| 110 | } | ||
| 111 | |||
| 112 | /* Set thread priority */ | ||
| 113 | android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); | ||
| 114 | |||
| 115 | } catch (Exception e) { | ||
| 116 | Log.v(TAG, "modify thread properties failed " + e.toString()); | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | public static native int nativeSetupJNI(); | ||
| 121 | |||
| 122 | public static native void removeAudioDevice(boolean recording, int deviceId); | ||
| 123 | |||
| 124 | public static native void addAudioDevice(boolean recording, String name, int deviceId); | ||
| 125 | |||
| 126 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java new file mode 100644 index 0000000..e1c892e --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java | |||
| @@ -0,0 +1,849 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import java.util.ArrayList; | ||
| 4 | import java.util.Collections; | ||
| 5 | import java.util.Comparator; | ||
| 6 | import java.util.List; | ||
| 7 | |||
| 8 | import android.content.Context; | ||
| 9 | import android.os.Build; | ||
| 10 | import android.os.VibrationEffect; | ||
| 11 | import android.os.Vibrator; | ||
| 12 | import android.os.VibratorManager; | ||
| 13 | import android.util.Log; | ||
| 14 | import android.view.InputDevice; | ||
| 15 | import android.view.KeyEvent; | ||
| 16 | import android.view.MotionEvent; | ||
| 17 | import android.view.View; | ||
| 18 | |||
| 19 | |||
| 20 | public class SDLControllerManager | ||
| 21 | { | ||
| 22 | |||
| 23 | public static native int nativeSetupJNI(); | ||
| 24 | |||
| 25 | public static native void nativeAddJoystick(int device_id, String name, String desc, | ||
| 26 | int vendor_id, int product_id, | ||
| 27 | int button_mask, | ||
| 28 | int naxes, int axis_mask, int nhats, boolean can_rumble); | ||
| 29 | public static native void nativeRemoveJoystick(int device_id); | ||
| 30 | public static native void nativeAddHaptic(int device_id, String name); | ||
| 31 | public static native void nativeRemoveHaptic(int device_id); | ||
| 32 | public static native boolean onNativePadDown(int device_id, int keycode); | ||
| 33 | public static native boolean onNativePadUp(int device_id, int keycode); | ||
| 34 | public static native void onNativeJoy(int device_id, int axis, | ||
| 35 | float value); | ||
| 36 | public static native void onNativeHat(int device_id, int hat_id, | ||
| 37 | int x, int y); | ||
| 38 | |||
| 39 | protected static SDLJoystickHandler mJoystickHandler; | ||
| 40 | protected static SDLHapticHandler mHapticHandler; | ||
| 41 | |||
| 42 | private static final String TAG = "SDLControllerManager"; | ||
| 43 | |||
| 44 | public static void initialize() { | ||
| 45 | if (mJoystickHandler == null) { | ||
| 46 | if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { | ||
| 47 | mJoystickHandler = new SDLJoystickHandler_API19(); | ||
| 48 | } else { | ||
| 49 | mJoystickHandler = new SDLJoystickHandler_API16(); | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | if (mHapticHandler == null) { | ||
| 54 | if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { | ||
| 55 | mHapticHandler = new SDLHapticHandler_API31(); | ||
| 56 | } else if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { | ||
| 57 | mHapticHandler = new SDLHapticHandler_API26(); | ||
| 58 | } else { | ||
| 59 | mHapticHandler = new SDLHapticHandler(); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance | ||
| 65 | public static boolean handleJoystickMotionEvent(MotionEvent event) { | ||
| 66 | return mJoystickHandler.handleMotionEvent(event); | ||
| 67 | } | ||
| 68 | |||
| 69 | /** | ||
| 70 | * This method is called by SDL using JNI. | ||
| 71 | */ | ||
| 72 | public static void pollInputDevices() { | ||
| 73 | mJoystickHandler.pollInputDevices(); | ||
| 74 | } | ||
| 75 | |||
| 76 | /** | ||
| 77 | * This method is called by SDL using JNI. | ||
| 78 | */ | ||
| 79 | public static void pollHapticDevices() { | ||
| 80 | mHapticHandler.pollHapticDevices(); | ||
| 81 | } | ||
| 82 | |||
| 83 | /** | ||
| 84 | * This method is called by SDL using JNI. | ||
| 85 | */ | ||
| 86 | public static void hapticRun(int device_id, float intensity, int length) { | ||
| 87 | mHapticHandler.run(device_id, intensity, length); | ||
| 88 | } | ||
| 89 | |||
| 90 | /** | ||
| 91 | * This method is called by SDL using JNI. | ||
| 92 | */ | ||
| 93 | public static void hapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { | ||
| 94 | mHapticHandler.rumble(device_id, low_frequency_intensity, high_frequency_intensity, length); | ||
| 95 | } | ||
| 96 | |||
| 97 | /** | ||
| 98 | * This method is called by SDL using JNI. | ||
| 99 | */ | ||
| 100 | public static void hapticStop(int device_id) | ||
| 101 | { | ||
| 102 | mHapticHandler.stop(device_id); | ||
| 103 | } | ||
| 104 | |||
| 105 | // Check if a given device is considered a possible SDL joystick | ||
| 106 | public static boolean isDeviceSDLJoystick(int deviceId) { | ||
| 107 | InputDevice device = InputDevice.getDevice(deviceId); | ||
| 108 | // We cannot use InputDevice.isVirtual before API 16, so let's accept | ||
| 109 | // only nonnegative device ids (VIRTUAL_KEYBOARD equals -1) | ||
| 110 | if ((device == null) || (deviceId < 0)) { | ||
| 111 | return false; | ||
| 112 | } | ||
| 113 | int sources = device.getSources(); | ||
| 114 | |||
| 115 | /* This is called for every button press, so let's not spam the logs */ | ||
| 116 | /* | ||
| 117 | if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { | ||
| 118 | Log.v(TAG, "Input device " + device.getName() + " has class joystick."); | ||
| 119 | } | ||
| 120 | if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) { | ||
| 121 | Log.v(TAG, "Input device " + device.getName() + " is a dpad."); | ||
| 122 | } | ||
| 123 | if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { | ||
| 124 | Log.v(TAG, "Input device " + device.getName() + " is a gamepad."); | ||
| 125 | } | ||
| 126 | */ | ||
| 127 | |||
| 128 | return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 || | ||
| 129 | ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) || | ||
| 130 | ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) | ||
| 131 | ); | ||
| 132 | } | ||
| 133 | |||
| 134 | } | ||
| 135 | |||
| 136 | class SDLJoystickHandler { | ||
| 137 | |||
| 138 | /** | ||
| 139 | * Handles given MotionEvent. | ||
| 140 | * @param event the event to be handled. | ||
| 141 | * @return if given event was processed. | ||
| 142 | */ | ||
| 143 | public boolean handleMotionEvent(MotionEvent event) { | ||
| 144 | return false; | ||
| 145 | } | ||
| 146 | |||
| 147 | /** | ||
| 148 | * Handles adding and removing of input devices. | ||
| 149 | */ | ||
| 150 | public void pollInputDevices() { | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | /* Actual joystick functionality available for API >= 12 devices */ | ||
| 155 | class SDLJoystickHandler_API16 extends SDLJoystickHandler { | ||
| 156 | |||
| 157 | static class SDLJoystick { | ||
| 158 | public int device_id; | ||
| 159 | public String name; | ||
| 160 | public String desc; | ||
| 161 | public ArrayList<InputDevice.MotionRange> axes; | ||
| 162 | public ArrayList<InputDevice.MotionRange> hats; | ||
| 163 | } | ||
| 164 | static class RangeComparator implements Comparator<InputDevice.MotionRange> { | ||
| 165 | @Override | ||
| 166 | public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { | ||
| 167 | // Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL | ||
| 168 | int arg0Axis = arg0.getAxis(); | ||
| 169 | int arg1Axis = arg1.getAxis(); | ||
| 170 | if (arg0Axis == MotionEvent.AXIS_GAS) { | ||
| 171 | arg0Axis = MotionEvent.AXIS_BRAKE; | ||
| 172 | } else if (arg0Axis == MotionEvent.AXIS_BRAKE) { | ||
| 173 | arg0Axis = MotionEvent.AXIS_GAS; | ||
| 174 | } | ||
| 175 | if (arg1Axis == MotionEvent.AXIS_GAS) { | ||
| 176 | arg1Axis = MotionEvent.AXIS_BRAKE; | ||
| 177 | } else if (arg1Axis == MotionEvent.AXIS_BRAKE) { | ||
| 178 | arg1Axis = MotionEvent.AXIS_GAS; | ||
| 179 | } | ||
| 180 | |||
| 181 | // Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ. | ||
| 182 | // This is because the usual pairing are: | ||
| 183 | // - AXIS_X + AXIS_Y (left stick). | ||
| 184 | // - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers). | ||
| 185 | // - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers). | ||
| 186 | // This sorts the axes in the above order, which tends to be correct | ||
| 187 | // for Xbox-ish game pads that have the right stick on RX/RY and the | ||
| 188 | // triggers on Z/RZ. | ||
| 189 | // | ||
| 190 | // Gamepads that don't have AXIS_Z/AXIS_RZ but use | ||
| 191 | // AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this. | ||
| 192 | // | ||
| 193 | // References: | ||
| 194 | // - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input | ||
| 195 | // - https://www.kernel.org/doc/html/latest/input/gamepad.html | ||
| 196 | if (arg0Axis == MotionEvent.AXIS_Z) { | ||
| 197 | arg0Axis = MotionEvent.AXIS_RZ - 1; | ||
| 198 | } else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) { | ||
| 199 | --arg0Axis; | ||
| 200 | } | ||
| 201 | if (arg1Axis == MotionEvent.AXIS_Z) { | ||
| 202 | arg1Axis = MotionEvent.AXIS_RZ - 1; | ||
| 203 | } else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) { | ||
| 204 | --arg1Axis; | ||
| 205 | } | ||
| 206 | |||
| 207 | return arg0Axis - arg1Axis; | ||
| 208 | } | ||
| 209 | } | ||
| 210 | |||
| 211 | private final ArrayList<SDLJoystick> mJoysticks; | ||
| 212 | |||
| 213 | public SDLJoystickHandler_API16() { | ||
| 214 | |||
| 215 | mJoysticks = new ArrayList<SDLJoystick>(); | ||
| 216 | } | ||
| 217 | |||
| 218 | @Override | ||
| 219 | public void pollInputDevices() { | ||
| 220 | int[] deviceIds = InputDevice.getDeviceIds(); | ||
| 221 | |||
| 222 | for (int device_id : deviceIds) { | ||
| 223 | if (SDLControllerManager.isDeviceSDLJoystick(device_id)) { | ||
| 224 | SDLJoystick joystick = getJoystick(device_id); | ||
| 225 | if (joystick == null) { | ||
| 226 | InputDevice joystickDevice = InputDevice.getDevice(device_id); | ||
| 227 | joystick = new SDLJoystick(); | ||
| 228 | joystick.device_id = device_id; | ||
| 229 | joystick.name = joystickDevice.getName(); | ||
| 230 | joystick.desc = getJoystickDescriptor(joystickDevice); | ||
| 231 | joystick.axes = new ArrayList<InputDevice.MotionRange>(); | ||
| 232 | joystick.hats = new ArrayList<InputDevice.MotionRange>(); | ||
| 233 | |||
| 234 | List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges(); | ||
| 235 | Collections.sort(ranges, new RangeComparator()); | ||
| 236 | for (InputDevice.MotionRange range : ranges) { | ||
| 237 | if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { | ||
| 238 | if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { | ||
| 239 | joystick.hats.add(range); | ||
| 240 | } else { | ||
| 241 | joystick.axes.add(range); | ||
| 242 | } | ||
| 243 | } | ||
| 244 | } | ||
| 245 | |||
| 246 | boolean can_rumble = false; | ||
| 247 | if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { | ||
| 248 | VibratorManager manager = joystickDevice.getVibratorManager(); | ||
| 249 | int[] vibrators = manager.getVibratorIds(); | ||
| 250 | if (vibrators.length > 0) { | ||
| 251 | can_rumble = true; | ||
| 252 | } | ||
| 253 | } | ||
| 254 | |||
| 255 | mJoysticks.add(joystick); | ||
| 256 | SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, | ||
| 257 | getVendorId(joystickDevice), getProductId(joystickDevice), | ||
| 258 | getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble); | ||
| 259 | } | ||
| 260 | } | ||
| 261 | } | ||
| 262 | |||
| 263 | /* Check removed devices */ | ||
| 264 | ArrayList<Integer> removedDevices = null; | ||
| 265 | for (SDLJoystick joystick : mJoysticks) { | ||
| 266 | int device_id = joystick.device_id; | ||
| 267 | int i; | ||
| 268 | for (i = 0; i < deviceIds.length; i++) { | ||
| 269 | if (device_id == deviceIds[i]) break; | ||
| 270 | } | ||
| 271 | if (i == deviceIds.length) { | ||
| 272 | if (removedDevices == null) { | ||
| 273 | removedDevices = new ArrayList<Integer>(); | ||
| 274 | } | ||
| 275 | removedDevices.add(device_id); | ||
| 276 | } | ||
| 277 | } | ||
| 278 | |||
| 279 | if (removedDevices != null) { | ||
| 280 | for (int device_id : removedDevices) { | ||
| 281 | SDLControllerManager.nativeRemoveJoystick(device_id); | ||
| 282 | for (int i = 0; i < mJoysticks.size(); i++) { | ||
| 283 | if (mJoysticks.get(i).device_id == device_id) { | ||
| 284 | mJoysticks.remove(i); | ||
| 285 | break; | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } | ||
| 289 | } | ||
| 290 | } | ||
| 291 | |||
| 292 | protected SDLJoystick getJoystick(int device_id) { | ||
| 293 | for (SDLJoystick joystick : mJoysticks) { | ||
| 294 | if (joystick.device_id == device_id) { | ||
| 295 | return joystick; | ||
| 296 | } | ||
| 297 | } | ||
| 298 | return null; | ||
| 299 | } | ||
| 300 | |||
| 301 | @Override | ||
| 302 | public boolean handleMotionEvent(MotionEvent event) { | ||
| 303 | int actionPointerIndex = event.getActionIndex(); | ||
| 304 | int action = event.getActionMasked(); | ||
| 305 | if (action == MotionEvent.ACTION_MOVE) { | ||
| 306 | SDLJoystick joystick = getJoystick(event.getDeviceId()); | ||
| 307 | if (joystick != null) { | ||
| 308 | for (int i = 0; i < joystick.axes.size(); i++) { | ||
| 309 | InputDevice.MotionRange range = joystick.axes.get(i); | ||
| 310 | /* Normalize the value to -1...1 */ | ||
| 311 | float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f; | ||
| 312 | SDLControllerManager.onNativeJoy(joystick.device_id, i, value); | ||
| 313 | } | ||
| 314 | for (int i = 0; i < joystick.hats.size() / 2; i++) { | ||
| 315 | int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex)); | ||
| 316 | int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex)); | ||
| 317 | SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY); | ||
| 318 | } | ||
| 319 | } | ||
| 320 | } | ||
| 321 | return true; | ||
| 322 | } | ||
| 323 | |||
| 324 | public String getJoystickDescriptor(InputDevice joystickDevice) { | ||
| 325 | String desc = joystickDevice.getDescriptor(); | ||
| 326 | |||
| 327 | if (desc != null && !desc.isEmpty()) { | ||
| 328 | return desc; | ||
| 329 | } | ||
| 330 | |||
| 331 | return joystickDevice.getName(); | ||
| 332 | } | ||
| 333 | public int getProductId(InputDevice joystickDevice) { | ||
| 334 | return 0; | ||
| 335 | } | ||
| 336 | public int getVendorId(InputDevice joystickDevice) { | ||
| 337 | return 0; | ||
| 338 | } | ||
| 339 | public int getAxisMask(List<InputDevice.MotionRange> ranges) { | ||
| 340 | return -1; | ||
| 341 | } | ||
| 342 | public int getButtonMask(InputDevice joystickDevice) { | ||
| 343 | return -1; | ||
| 344 | } | ||
| 345 | } | ||
| 346 | |||
| 347 | class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 { | ||
| 348 | |||
| 349 | @Override | ||
| 350 | public int getProductId(InputDevice joystickDevice) { | ||
| 351 | return joystickDevice.getProductId(); | ||
| 352 | } | ||
| 353 | |||
| 354 | @Override | ||
| 355 | public int getVendorId(InputDevice joystickDevice) { | ||
| 356 | return joystickDevice.getVendorId(); | ||
| 357 | } | ||
| 358 | |||
| 359 | @Override | ||
| 360 | public int getAxisMask(List<InputDevice.MotionRange> ranges) { | ||
| 361 | // For compatibility, keep computing the axis mask like before, | ||
| 362 | // only really distinguishing 2, 4 and 6 axes. | ||
| 363 | int axis_mask = 0; | ||
| 364 | if (ranges.size() >= 2) { | ||
| 365 | // ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY)) | ||
| 366 | axis_mask |= 0x0003; | ||
| 367 | } | ||
| 368 | if (ranges.size() >= 4) { | ||
| 369 | // ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY)) | ||
| 370 | axis_mask |= 0x000c; | ||
| 371 | } | ||
| 372 | if (ranges.size() >= 6) { | ||
| 373 | // ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) | ||
| 374 | axis_mask |= 0x0030; | ||
| 375 | } | ||
| 376 | // Also add an indicator bit for whether the sorting order has changed. | ||
| 377 | // This serves to disable outdated gamecontrollerdb.txt mappings. | ||
| 378 | boolean have_z = false; | ||
| 379 | boolean have_past_z_before_rz = false; | ||
| 380 | for (InputDevice.MotionRange range : ranges) { | ||
| 381 | int axis = range.getAxis(); | ||
| 382 | if (axis == MotionEvent.AXIS_Z) { | ||
| 383 | have_z = true; | ||
| 384 | } else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) { | ||
| 385 | have_past_z_before_rz = true; | ||
| 386 | } | ||
| 387 | } | ||
| 388 | if (have_z && have_past_z_before_rz) { | ||
| 389 | // If both these exist, the compare() function changed sorting order. | ||
| 390 | // Set a bit to indicate this fact. | ||
| 391 | axis_mask |= 0x8000; | ||
| 392 | } | ||
| 393 | return axis_mask; | ||
| 394 | } | ||
| 395 | |||
| 396 | @Override | ||
| 397 | public int getButtonMask(InputDevice joystickDevice) { | ||
| 398 | int button_mask = 0; | ||
| 399 | int[] keys = new int[] { | ||
| 400 | KeyEvent.KEYCODE_BUTTON_A, | ||
| 401 | KeyEvent.KEYCODE_BUTTON_B, | ||
| 402 | KeyEvent.KEYCODE_BUTTON_X, | ||
| 403 | KeyEvent.KEYCODE_BUTTON_Y, | ||
| 404 | KeyEvent.KEYCODE_BACK, | ||
| 405 | KeyEvent.KEYCODE_MENU, | ||
| 406 | KeyEvent.KEYCODE_BUTTON_MODE, | ||
| 407 | KeyEvent.KEYCODE_BUTTON_START, | ||
| 408 | KeyEvent.KEYCODE_BUTTON_THUMBL, | ||
| 409 | KeyEvent.KEYCODE_BUTTON_THUMBR, | ||
| 410 | KeyEvent.KEYCODE_BUTTON_L1, | ||
| 411 | KeyEvent.KEYCODE_BUTTON_R1, | ||
| 412 | KeyEvent.KEYCODE_DPAD_UP, | ||
| 413 | KeyEvent.KEYCODE_DPAD_DOWN, | ||
| 414 | KeyEvent.KEYCODE_DPAD_LEFT, | ||
| 415 | KeyEvent.KEYCODE_DPAD_RIGHT, | ||
| 416 | KeyEvent.KEYCODE_BUTTON_SELECT, | ||
| 417 | KeyEvent.KEYCODE_DPAD_CENTER, | ||
| 418 | |||
| 419 | // These don't map into any SDL controller buttons directly | ||
| 420 | KeyEvent.KEYCODE_BUTTON_L2, | ||
| 421 | KeyEvent.KEYCODE_BUTTON_R2, | ||
| 422 | KeyEvent.KEYCODE_BUTTON_C, | ||
| 423 | KeyEvent.KEYCODE_BUTTON_Z, | ||
| 424 | KeyEvent.KEYCODE_BUTTON_1, | ||
| 425 | KeyEvent.KEYCODE_BUTTON_2, | ||
| 426 | KeyEvent.KEYCODE_BUTTON_3, | ||
| 427 | KeyEvent.KEYCODE_BUTTON_4, | ||
| 428 | KeyEvent.KEYCODE_BUTTON_5, | ||
| 429 | KeyEvent.KEYCODE_BUTTON_6, | ||
| 430 | KeyEvent.KEYCODE_BUTTON_7, | ||
| 431 | KeyEvent.KEYCODE_BUTTON_8, | ||
| 432 | KeyEvent.KEYCODE_BUTTON_9, | ||
| 433 | KeyEvent.KEYCODE_BUTTON_10, | ||
| 434 | KeyEvent.KEYCODE_BUTTON_11, | ||
| 435 | KeyEvent.KEYCODE_BUTTON_12, | ||
| 436 | KeyEvent.KEYCODE_BUTTON_13, | ||
| 437 | KeyEvent.KEYCODE_BUTTON_14, | ||
| 438 | KeyEvent.KEYCODE_BUTTON_15, | ||
| 439 | KeyEvent.KEYCODE_BUTTON_16, | ||
| 440 | }; | ||
| 441 | int[] masks = new int[] { | ||
| 442 | (1 << 0), // A -> A | ||
| 443 | (1 << 1), // B -> B | ||
| 444 | (1 << 2), // X -> X | ||
| 445 | (1 << 3), // Y -> Y | ||
| 446 | (1 << 4), // BACK -> BACK | ||
| 447 | (1 << 6), // MENU -> START | ||
| 448 | (1 << 5), // MODE -> GUIDE | ||
| 449 | (1 << 6), // START -> START | ||
| 450 | (1 << 7), // THUMBL -> LEFTSTICK | ||
| 451 | (1 << 8), // THUMBR -> RIGHTSTICK | ||
| 452 | (1 << 9), // L1 -> LEFTSHOULDER | ||
| 453 | (1 << 10), // R1 -> RIGHTSHOULDER | ||
| 454 | (1 << 11), // DPAD_UP -> DPAD_UP | ||
| 455 | (1 << 12), // DPAD_DOWN -> DPAD_DOWN | ||
| 456 | (1 << 13), // DPAD_LEFT -> DPAD_LEFT | ||
| 457 | (1 << 14), // DPAD_RIGHT -> DPAD_RIGHT | ||
| 458 | (1 << 4), // SELECT -> BACK | ||
| 459 | (1 << 0), // DPAD_CENTER -> A | ||
| 460 | (1 << 15), // L2 -> ?? | ||
| 461 | (1 << 16), // R2 -> ?? | ||
| 462 | (1 << 17), // C -> ?? | ||
| 463 | (1 << 18), // Z -> ?? | ||
| 464 | (1 << 20), // 1 -> ?? | ||
| 465 | (1 << 21), // 2 -> ?? | ||
| 466 | (1 << 22), // 3 -> ?? | ||
| 467 | (1 << 23), // 4 -> ?? | ||
| 468 | (1 << 24), // 5 -> ?? | ||
| 469 | (1 << 25), // 6 -> ?? | ||
| 470 | (1 << 26), // 7 -> ?? | ||
| 471 | (1 << 27), // 8 -> ?? | ||
| 472 | (1 << 28), // 9 -> ?? | ||
| 473 | (1 << 29), // 10 -> ?? | ||
| 474 | (1 << 30), // 11 -> ?? | ||
| 475 | (1 << 31), // 12 -> ?? | ||
| 476 | // We're out of room... | ||
| 477 | 0xFFFFFFFF, // 13 -> ?? | ||
| 478 | 0xFFFFFFFF, // 14 -> ?? | ||
| 479 | 0xFFFFFFFF, // 15 -> ?? | ||
| 480 | 0xFFFFFFFF, // 16 -> ?? | ||
| 481 | }; | ||
| 482 | boolean[] has_keys = joystickDevice.hasKeys(keys); | ||
| 483 | for (int i = 0; i < keys.length; ++i) { | ||
| 484 | if (has_keys[i]) { | ||
| 485 | button_mask |= masks[i]; | ||
| 486 | } | ||
| 487 | } | ||
| 488 | return button_mask; | ||
| 489 | } | ||
| 490 | } | ||
| 491 | |||
| 492 | class SDLHapticHandler_API31 extends SDLHapticHandler { | ||
| 493 | @Override | ||
| 494 | public void run(int device_id, float intensity, int length) { | ||
| 495 | SDLHaptic haptic = getHaptic(device_id); | ||
| 496 | if (haptic != null) { | ||
| 497 | vibrate(haptic.vib, intensity, length); | ||
| 498 | } | ||
| 499 | } | ||
| 500 | |||
| 501 | @Override | ||
| 502 | public void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { | ||
| 503 | InputDevice device = InputDevice.getDevice(device_id); | ||
| 504 | if (device == null) { | ||
| 505 | return; | ||
| 506 | } | ||
| 507 | |||
| 508 | VibratorManager manager = device.getVibratorManager(); | ||
| 509 | int[] vibrators = manager.getVibratorIds(); | ||
| 510 | if (vibrators.length >= 2) { | ||
| 511 | vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length); | ||
| 512 | vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length); | ||
| 513 | } else if (vibrators.length == 1) { | ||
| 514 | float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f); | ||
| 515 | vibrate(manager.getVibrator(vibrators[0]), intensity, length); | ||
| 516 | } | ||
| 517 | } | ||
| 518 | |||
| 519 | private void vibrate(Vibrator vibrator, float intensity, int length) { | ||
| 520 | if (intensity == 0.0f) { | ||
| 521 | vibrator.cancel(); | ||
| 522 | return; | ||
| 523 | } | ||
| 524 | |||
| 525 | int value = Math.round(intensity * 255); | ||
| 526 | if (value > 255) { | ||
| 527 | value = 255; | ||
| 528 | } | ||
| 529 | if (value < 1) { | ||
| 530 | vibrator.cancel(); | ||
| 531 | return; | ||
| 532 | } | ||
| 533 | try { | ||
| 534 | vibrator.vibrate(VibrationEffect.createOneShot(length, value)); | ||
| 535 | } | ||
| 536 | catch (Exception e) { | ||
| 537 | // Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if | ||
| 538 | // something went horribly wrong with the Android 8.0 APIs. | ||
| 539 | vibrator.vibrate(length); | ||
| 540 | } | ||
| 541 | } | ||
| 542 | } | ||
| 543 | |||
| 544 | class SDLHapticHandler_API26 extends SDLHapticHandler { | ||
| 545 | @Override | ||
| 546 | public void run(int device_id, float intensity, int length) { | ||
| 547 | SDLHaptic haptic = getHaptic(device_id); | ||
| 548 | if (haptic != null) { | ||
| 549 | if (intensity == 0.0f) { | ||
| 550 | stop(device_id); | ||
| 551 | return; | ||
| 552 | } | ||
| 553 | |||
| 554 | int vibeValue = Math.round(intensity * 255); | ||
| 555 | |||
| 556 | if (vibeValue > 255) { | ||
| 557 | vibeValue = 255; | ||
| 558 | } | ||
| 559 | if (vibeValue < 1) { | ||
| 560 | stop(device_id); | ||
| 561 | return; | ||
| 562 | } | ||
| 563 | try { | ||
| 564 | haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue)); | ||
| 565 | } | ||
| 566 | catch (Exception e) { | ||
| 567 | // Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if | ||
| 568 | // something went horribly wrong with the Android 8.0 APIs. | ||
| 569 | haptic.vib.vibrate(length); | ||
| 570 | } | ||
| 571 | } | ||
| 572 | } | ||
| 573 | } | ||
| 574 | |||
| 575 | class SDLHapticHandler { | ||
| 576 | |||
| 577 | static class SDLHaptic { | ||
| 578 | public int device_id; | ||
| 579 | public String name; | ||
| 580 | public Vibrator vib; | ||
| 581 | } | ||
| 582 | |||
| 583 | private final ArrayList<SDLHaptic> mHaptics; | ||
| 584 | |||
| 585 | public SDLHapticHandler() { | ||
| 586 | mHaptics = new ArrayList<SDLHaptic>(); | ||
| 587 | } | ||
| 588 | |||
| 589 | public void run(int device_id, float intensity, int length) { | ||
| 590 | SDLHaptic haptic = getHaptic(device_id); | ||
| 591 | if (haptic != null) { | ||
| 592 | haptic.vib.vibrate(length); | ||
| 593 | } | ||
| 594 | } | ||
| 595 | |||
| 596 | public void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { | ||
| 597 | // Not supported in older APIs | ||
| 598 | } | ||
| 599 | |||
| 600 | public void stop(int device_id) { | ||
| 601 | SDLHaptic haptic = getHaptic(device_id); | ||
| 602 | if (haptic != null) { | ||
| 603 | haptic.vib.cancel(); | ||
| 604 | } | ||
| 605 | } | ||
| 606 | |||
| 607 | public void pollHapticDevices() { | ||
| 608 | |||
| 609 | final int deviceId_VIBRATOR_SERVICE = 999999; | ||
| 610 | boolean hasVibratorService = false; | ||
| 611 | |||
| 612 | /* Check VIBRATOR_SERVICE */ | ||
| 613 | Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE); | ||
| 614 | if (vib != null) { | ||
| 615 | hasVibratorService = vib.hasVibrator(); | ||
| 616 | |||
| 617 | if (hasVibratorService) { | ||
| 618 | SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE); | ||
| 619 | if (haptic == null) { | ||
| 620 | haptic = new SDLHaptic(); | ||
| 621 | haptic.device_id = deviceId_VIBRATOR_SERVICE; | ||
| 622 | haptic.name = "VIBRATOR_SERVICE"; | ||
| 623 | haptic.vib = vib; | ||
| 624 | mHaptics.add(haptic); | ||
| 625 | SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name); | ||
| 626 | } | ||
| 627 | } | ||
| 628 | } | ||
| 629 | |||
| 630 | /* Check removed devices */ | ||
| 631 | ArrayList<Integer> removedDevices = null; | ||
| 632 | for (SDLHaptic haptic : mHaptics) { | ||
| 633 | int device_id = haptic.device_id; | ||
| 634 | if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) { | ||
| 635 | if (removedDevices == null) { | ||
| 636 | removedDevices = new ArrayList<Integer>(); | ||
| 637 | } | ||
| 638 | removedDevices.add(device_id); | ||
| 639 | } // else: don't remove the vibrator if it is still present | ||
| 640 | } | ||
| 641 | |||
| 642 | if (removedDevices != null) { | ||
| 643 | for (int device_id : removedDevices) { | ||
| 644 | SDLControllerManager.nativeRemoveHaptic(device_id); | ||
| 645 | for (int i = 0; i < mHaptics.size(); i++) { | ||
| 646 | if (mHaptics.get(i).device_id == device_id) { | ||
| 647 | mHaptics.remove(i); | ||
| 648 | break; | ||
| 649 | } | ||
| 650 | } | ||
| 651 | } | ||
| 652 | } | ||
| 653 | } | ||
| 654 | |||
| 655 | protected SDLHaptic getHaptic(int device_id) { | ||
| 656 | for (SDLHaptic haptic : mHaptics) { | ||
| 657 | if (haptic.device_id == device_id) { | ||
| 658 | return haptic; | ||
| 659 | } | ||
| 660 | } | ||
| 661 | return null; | ||
| 662 | } | ||
| 663 | } | ||
| 664 | |||
| 665 | class SDLGenericMotionListener_API14 implements View.OnGenericMotionListener { | ||
| 666 | // Generic Motion (mouse hover, joystick...) events go here | ||
| 667 | @Override | ||
| 668 | public boolean onGenericMotion(View v, MotionEvent event) { | ||
| 669 | if (event.getSource() == InputDevice.SOURCE_JOYSTICK) | ||
| 670 | return SDLControllerManager.handleJoystickMotionEvent(event); | ||
| 671 | |||
| 672 | float x, y; | ||
| 673 | int action = event.getActionMasked(); | ||
| 674 | int pointerCount = event.getPointerCount(); | ||
| 675 | boolean consumed = false; | ||
| 676 | |||
| 677 | for (int i = 0; i < pointerCount; i++) { | ||
| 678 | int toolType = event.getToolType(i); | ||
| 679 | |||
| 680 | if (toolType == MotionEvent.TOOL_TYPE_MOUSE) { | ||
| 681 | switch (action) { | ||
| 682 | case MotionEvent.ACTION_SCROLL: | ||
| 683 | x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i); | ||
| 684 | y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i); | ||
| 685 | SDLActivity.onNativeMouse(0, action, x, y, false); | ||
| 686 | consumed = true; | ||
| 687 | break; | ||
| 688 | |||
| 689 | case MotionEvent.ACTION_HOVER_MOVE: | ||
| 690 | x = getEventX(event, i); | ||
| 691 | y = getEventY(event, i); | ||
| 692 | |||
| 693 | SDLActivity.onNativeMouse(0, action, x, y, checkRelativeEvent(event)); | ||
| 694 | consumed = true; | ||
| 695 | break; | ||
| 696 | |||
| 697 | default: | ||
| 698 | break; | ||
| 699 | } | ||
| 700 | } else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) { | ||
| 701 | switch (action) { | ||
| 702 | case MotionEvent.ACTION_HOVER_ENTER: | ||
| 703 | case MotionEvent.ACTION_HOVER_MOVE: | ||
| 704 | case MotionEvent.ACTION_HOVER_EXIT: | ||
| 705 | x = event.getX(i); | ||
| 706 | y = event.getY(i); | ||
| 707 | float p = event.getPressure(i); | ||
| 708 | if (p > 1.0f) { | ||
| 709 | // may be larger than 1.0f on some devices | ||
| 710 | // see the documentation of getPressure(i) | ||
| 711 | p = 1.0f; | ||
| 712 | } | ||
| 713 | |||
| 714 | // BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP | ||
| 715 | int buttons = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30)); | ||
| 716 | |||
| 717 | SDLActivity.onNativePen(event.getPointerId(i), buttons, action, x, y, p); | ||
| 718 | consumed = true; | ||
| 719 | break; | ||
| 720 | } | ||
| 721 | } | ||
| 722 | } | ||
| 723 | |||
| 724 | return consumed; | ||
| 725 | } | ||
| 726 | |||
| 727 | public boolean supportsRelativeMouse() { | ||
| 728 | return false; | ||
| 729 | } | ||
| 730 | |||
| 731 | public boolean inRelativeMode() { | ||
| 732 | return false; | ||
| 733 | } | ||
| 734 | |||
| 735 | public boolean setRelativeMouseEnabled(boolean enabled) { | ||
| 736 | return false; | ||
| 737 | } | ||
| 738 | |||
| 739 | public void reclaimRelativeMouseModeIfNeeded() { | ||
| 740 | |||
| 741 | } | ||
| 742 | |||
| 743 | public boolean checkRelativeEvent(MotionEvent event) { | ||
| 744 | return inRelativeMode(); | ||
| 745 | } | ||
| 746 | |||
| 747 | public float getEventX(MotionEvent event, int pointerIndex) { | ||
| 748 | return event.getX(pointerIndex); | ||
| 749 | } | ||
| 750 | |||
| 751 | public float getEventY(MotionEvent event, int pointerIndex) { | ||
| 752 | return event.getY(pointerIndex); | ||
| 753 | } | ||
| 754 | |||
| 755 | } | ||
| 756 | |||
| 757 | class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API14 { | ||
| 758 | // Generic Motion (mouse hover, joystick...) events go here | ||
| 759 | |||
| 760 | private boolean mRelativeModeEnabled; | ||
| 761 | |||
| 762 | @Override | ||
| 763 | public boolean supportsRelativeMouse() { | ||
| 764 | return true; | ||
| 765 | } | ||
| 766 | |||
| 767 | @Override | ||
| 768 | public boolean inRelativeMode() { | ||
| 769 | return mRelativeModeEnabled; | ||
| 770 | } | ||
| 771 | |||
| 772 | @Override | ||
| 773 | public boolean setRelativeMouseEnabled(boolean enabled) { | ||
| 774 | mRelativeModeEnabled = enabled; | ||
| 775 | return true; | ||
| 776 | } | ||
| 777 | |||
| 778 | @Override | ||
| 779 | public float getEventX(MotionEvent event, int pointerIndex) { | ||
| 780 | if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) { | ||
| 781 | return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X, pointerIndex); | ||
| 782 | } else { | ||
| 783 | return event.getX(pointerIndex); | ||
| 784 | } | ||
| 785 | } | ||
| 786 | |||
| 787 | @Override | ||
| 788 | public float getEventY(MotionEvent event, int pointerIndex) { | ||
| 789 | if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) { | ||
| 790 | return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y, pointerIndex); | ||
| 791 | } else { | ||
| 792 | return event.getY(pointerIndex); | ||
| 793 | } | ||
| 794 | } | ||
| 795 | } | ||
| 796 | |||
| 797 | class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 { | ||
| 798 | // Generic Motion (mouse hover, joystick...) events go here | ||
| 799 | private boolean mRelativeModeEnabled; | ||
| 800 | |||
| 801 | @Override | ||
| 802 | public boolean supportsRelativeMouse() { | ||
| 803 | return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */); | ||
| 804 | } | ||
| 805 | |||
| 806 | @Override | ||
| 807 | public boolean inRelativeMode() { | ||
| 808 | return mRelativeModeEnabled; | ||
| 809 | } | ||
| 810 | |||
| 811 | @Override | ||
| 812 | public boolean setRelativeMouseEnabled(boolean enabled) { | ||
| 813 | if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) { | ||
| 814 | if (enabled) { | ||
| 815 | SDLActivity.getContentView().requestPointerCapture(); | ||
| 816 | } else { | ||
| 817 | SDLActivity.getContentView().releasePointerCapture(); | ||
| 818 | } | ||
| 819 | mRelativeModeEnabled = enabled; | ||
| 820 | return true; | ||
| 821 | } else { | ||
| 822 | return false; | ||
| 823 | } | ||
| 824 | } | ||
| 825 | |||
| 826 | @Override | ||
| 827 | public void reclaimRelativeMouseModeIfNeeded() { | ||
| 828 | if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) { | ||
| 829 | SDLActivity.getContentView().requestPointerCapture(); | ||
| 830 | } | ||
| 831 | } | ||
| 832 | |||
| 833 | @Override | ||
| 834 | public boolean checkRelativeEvent(MotionEvent event) { | ||
| 835 | return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE; | ||
| 836 | } | ||
| 837 | |||
| 838 | @Override | ||
| 839 | public float getEventX(MotionEvent event, int pointerIndex) { | ||
| 840 | // Relative mouse in capture mode will only have relative for X/Y | ||
| 841 | return event.getX(pointerIndex); | ||
| 842 | } | ||
| 843 | |||
| 844 | @Override | ||
| 845 | public float getEventY(MotionEvent event, int pointerIndex) { | ||
| 846 | // Relative mouse in capture mode will only have relative for X/Y | ||
| 847 | return event.getY(pointerIndex); | ||
| 848 | } | ||
| 849 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java new file mode 100644 index 0000000..40e556f --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.content.*; | ||
| 4 | import android.text.InputType; | ||
| 5 | import android.view.*; | ||
| 6 | import android.view.inputmethod.EditorInfo; | ||
| 7 | import android.view.inputmethod.InputConnection; | ||
| 8 | |||
| 9 | /* This is a fake invisible editor view that receives the input and defines the | ||
| 10 | * pan&scan region | ||
| 11 | */ | ||
| 12 | public class SDLDummyEdit extends View implements View.OnKeyListener | ||
| 13 | { | ||
| 14 | InputConnection ic; | ||
| 15 | int input_type; | ||
| 16 | |||
| 17 | public SDLDummyEdit(Context context) { | ||
| 18 | super(context); | ||
| 19 | setFocusableInTouchMode(true); | ||
| 20 | setFocusable(true); | ||
| 21 | setOnKeyListener(this); | ||
| 22 | } | ||
| 23 | |||
| 24 | public void setInputType(int input_type) { | ||
| 25 | this.input_type = input_type; | ||
| 26 | } | ||
| 27 | |||
| 28 | @Override | ||
| 29 | public boolean onCheckIsTextEditor() { | ||
| 30 | return true; | ||
| 31 | } | ||
| 32 | |||
| 33 | @Override | ||
| 34 | public boolean onKey(View v, int keyCode, KeyEvent event) { | ||
| 35 | return SDLActivity.handleKeyEvent(v, keyCode, event, ic); | ||
| 36 | } | ||
| 37 | |||
| 38 | // | ||
| 39 | @Override | ||
| 40 | public boolean onKeyPreIme (int keyCode, KeyEvent event) { | ||
| 41 | // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event | ||
| 42 | // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639 | ||
| 43 | // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not | ||
| 44 | // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout | ||
| 45 | // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android | ||
| 46 | // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :) | ||
| 47 | if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { | ||
| 48 | if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) { | ||
| 49 | SDLActivity.onNativeKeyboardFocusLost(); | ||
| 50 | } | ||
| 51 | } | ||
| 52 | return super.onKeyPreIme(keyCode, event); | ||
| 53 | } | ||
| 54 | |||
| 55 | @Override | ||
| 56 | public InputConnection onCreateInputConnection(EditorInfo outAttrs) { | ||
| 57 | ic = new SDLInputConnection(this, true); | ||
| 58 | |||
| 59 | outAttrs.inputType = input_type; | ||
| 60 | outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | | ||
| 61 | EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; | ||
| 62 | |||
| 63 | return ic; | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java new file mode 100644 index 0000000..accce4b --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java | |||
| @@ -0,0 +1,138 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | import android.content.*; | ||
| 4 | import android.os.Build; | ||
| 5 | import android.text.Editable; | ||
| 6 | import android.view.*; | ||
| 7 | import android.view.inputmethod.BaseInputConnection; | ||
| 8 | import android.widget.EditText; | ||
| 9 | |||
| 10 | public class SDLInputConnection extends BaseInputConnection | ||
| 11 | { | ||
| 12 | protected EditText mEditText; | ||
| 13 | protected String mCommittedText = ""; | ||
| 14 | |||
| 15 | public SDLInputConnection(View targetView, boolean fullEditor) { | ||
| 16 | super(targetView, fullEditor); | ||
| 17 | mEditText = new EditText(SDL.getContext()); | ||
| 18 | } | ||
| 19 | |||
| 20 | @Override | ||
| 21 | public Editable getEditable() { | ||
| 22 | return mEditText.getEditableText(); | ||
| 23 | } | ||
| 24 | |||
| 25 | @Override | ||
| 26 | public boolean sendKeyEvent(KeyEvent event) { | ||
| 27 | /* | ||
| 28 | * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard) | ||
| 29 | * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses | ||
| 30 | * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys | ||
| 31 | * that still do, we empty this out. | ||
| 32 | */ | ||
| 33 | |||
| 34 | /* | ||
| 35 | * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key | ||
| 36 | * as we do with physical keyboards, let's just use it to hide the keyboard. | ||
| 37 | */ | ||
| 38 | |||
| 39 | if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { | ||
| 40 | if (SDLActivity.onNativeSoftReturnKey()) { | ||
| 41 | return true; | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | return super.sendKeyEvent(event); | ||
| 46 | } | ||
| 47 | |||
| 48 | @Override | ||
| 49 | public boolean commitText(CharSequence text, int newCursorPosition) { | ||
| 50 | if (!super.commitText(text, newCursorPosition)) { | ||
| 51 | return false; | ||
| 52 | } | ||
| 53 | updateText(); | ||
| 54 | return true; | ||
| 55 | } | ||
| 56 | |||
| 57 | @Override | ||
| 58 | public boolean setComposingText(CharSequence text, int newCursorPosition) { | ||
| 59 | if (!super.setComposingText(text, newCursorPosition)) { | ||
| 60 | return false; | ||
| 61 | } | ||
| 62 | updateText(); | ||
| 63 | return true; | ||
| 64 | } | ||
| 65 | |||
| 66 | @Override | ||
| 67 | public boolean deleteSurroundingText(int beforeLength, int afterLength) { | ||
| 68 | if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) { | ||
| 69 | // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection | ||
| 70 | // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 | ||
| 71 | if (beforeLength > 0 && afterLength == 0) { | ||
| 72 | // backspace(s) | ||
| 73 | while (beforeLength-- > 0) { | ||
| 74 | nativeGenerateScancodeForUnichar('\b'); | ||
| 75 | } | ||
| 76 | return true; | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | if (!super.deleteSurroundingText(beforeLength, afterLength)) { | ||
| 81 | return false; | ||
| 82 | } | ||
| 83 | updateText(); | ||
| 84 | return true; | ||
| 85 | } | ||
| 86 | |||
| 87 | protected void updateText() { | ||
| 88 | final Editable content = getEditable(); | ||
| 89 | if (content == null) { | ||
| 90 | return; | ||
| 91 | } | ||
| 92 | |||
| 93 | String text = content.toString(); | ||
| 94 | int compareLength = Math.min(text.length(), mCommittedText.length()); | ||
| 95 | int matchLength, offset; | ||
| 96 | |||
| 97 | /* Backspace over characters that are no longer in the string */ | ||
| 98 | for (matchLength = 0; matchLength < compareLength; ) { | ||
| 99 | int codePoint = mCommittedText.codePointAt(matchLength); | ||
| 100 | if (codePoint != text.codePointAt(matchLength)) { | ||
| 101 | break; | ||
| 102 | } | ||
| 103 | matchLength += Character.charCount(codePoint); | ||
| 104 | } | ||
| 105 | /* FIXME: This doesn't handle graphemes, like '🌬️' */ | ||
| 106 | for (offset = matchLength; offset < mCommittedText.length(); ) { | ||
| 107 | int codePoint = mCommittedText.codePointAt(offset); | ||
| 108 | nativeGenerateScancodeForUnichar('\b'); | ||
| 109 | offset += Character.charCount(codePoint); | ||
| 110 | } | ||
| 111 | |||
| 112 | if (matchLength < text.length()) { | ||
| 113 | String pendingText = text.subSequence(matchLength, text.length()).toString(); | ||
| 114 | if (!SDLActivity.dispatchingKeyEvent()) { | ||
| 115 | for (offset = 0; offset < pendingText.length(); ) { | ||
| 116 | int codePoint = pendingText.codePointAt(offset); | ||
| 117 | if (codePoint == '\n') { | ||
| 118 | if (SDLActivity.onNativeSoftReturnKey()) { | ||
| 119 | return; | ||
| 120 | } | ||
| 121 | } | ||
| 122 | /* Higher code points don't generate simulated scancodes */ | ||
| 123 | if (codePoint > 0 && codePoint < 128) { | ||
| 124 | nativeGenerateScancodeForUnichar((char)codePoint); | ||
| 125 | } | ||
| 126 | offset += Character.charCount(codePoint); | ||
| 127 | } | ||
| 128 | } | ||
| 129 | SDLInputConnection.nativeCommitText(pendingText, 0); | ||
| 130 | } | ||
| 131 | mCommittedText = text; | ||
| 132 | } | ||
| 133 | |||
| 134 | public static native void nativeCommitText(String text, int newCursorPosition); | ||
| 135 | |||
| 136 | public static native void nativeGenerateScancodeForUnichar(char c); | ||
| 137 | } | ||
| 138 | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java new file mode 100644 index 0000000..080501c --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java | |||
| @@ -0,0 +1,408 @@ | |||
| 1 | package org.libsdl.app; | ||
| 2 | |||
| 3 | |||
| 4 | import android.content.Context; | ||
| 5 | import android.content.pm.ActivityInfo; | ||
| 6 | import android.graphics.Insets; | ||
| 7 | import android.hardware.Sensor; | ||
| 8 | import android.hardware.SensorEvent; | ||
| 9 | import android.hardware.SensorEventListener; | ||
| 10 | import android.hardware.SensorManager; | ||
| 11 | import android.os.Build; | ||
| 12 | import android.util.DisplayMetrics; | ||
| 13 | import android.util.Log; | ||
| 14 | import android.view.Display; | ||
| 15 | import android.view.InputDevice; | ||
| 16 | import android.view.KeyEvent; | ||
| 17 | import android.view.MotionEvent; | ||
| 18 | import android.view.Surface; | ||
| 19 | import android.view.SurfaceHolder; | ||
| 20 | import android.view.SurfaceView; | ||
| 21 | import android.view.View; | ||
| 22 | import android.view.WindowInsets; | ||
| 23 | import android.view.WindowManager; | ||
| 24 | |||
| 25 | |||
| 26 | /** | ||
| 27 | SDLSurface. This is what we draw on, so we need to know when it's created | ||
| 28 | in order to do anything useful. | ||
| 29 | |||
| 30 | Because of this, that's where we set up the SDL thread | ||
| 31 | */ | ||
| 32 | public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, | ||
| 33 | View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener, SensorEventListener { | ||
| 34 | |||
| 35 | // Sensors | ||
| 36 | protected SensorManager mSensorManager; | ||
| 37 | protected Display mDisplay; | ||
| 38 | |||
| 39 | // Keep track of the surface size to normalize touch events | ||
| 40 | protected float mWidth, mHeight; | ||
| 41 | |||
| 42 | // Is SurfaceView ready for rendering | ||
| 43 | public boolean mIsSurfaceReady; | ||
| 44 | |||
| 45 | // Startup | ||
| 46 | public SDLSurface(Context context) { | ||
| 47 | super(context); | ||
| 48 | getHolder().addCallback(this); | ||
| 49 | |||
| 50 | setFocusable(true); | ||
| 51 | setFocusableInTouchMode(true); | ||
| 52 | requestFocus(); | ||
| 53 | setOnApplyWindowInsetsListener(this); | ||
| 54 | setOnKeyListener(this); | ||
| 55 | setOnTouchListener(this); | ||
| 56 | |||
| 57 | mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); | ||
| 58 | mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); | ||
| 59 | |||
| 60 | setOnGenericMotionListener(SDLActivity.getMotionListener()); | ||
| 61 | |||
| 62 | // Some arbitrary defaults to avoid a potential division by zero | ||
| 63 | mWidth = 1.0f; | ||
| 64 | mHeight = 1.0f; | ||
| 65 | |||
| 66 | mIsSurfaceReady = false; | ||
| 67 | } | ||
| 68 | |||
| 69 | public void handlePause() { | ||
| 70 | enableSensor(Sensor.TYPE_ACCELEROMETER, false); | ||
| 71 | } | ||
| 72 | |||
| 73 | public void handleResume() { | ||
| 74 | setFocusable(true); | ||
| 75 | setFocusableInTouchMode(true); | ||
| 76 | requestFocus(); | ||
| 77 | setOnApplyWindowInsetsListener(this); | ||
| 78 | setOnKeyListener(this); | ||
| 79 | setOnTouchListener(this); | ||
| 80 | enableSensor(Sensor.TYPE_ACCELEROMETER, true); | ||
| 81 | } | ||
| 82 | |||
| 83 | public Surface getNativeSurface() { | ||
| 84 | return getHolder().getSurface(); | ||
| 85 | } | ||
| 86 | |||
| 87 | // Called when we have a valid drawing surface | ||
| 88 | @Override | ||
| 89 | public void surfaceCreated(SurfaceHolder holder) { | ||
| 90 | Log.v("SDL", "surfaceCreated()"); | ||
| 91 | SDLActivity.onNativeSurfaceCreated(); | ||
| 92 | } | ||
| 93 | |||
| 94 | // Called when we lose the surface | ||
| 95 | @Override | ||
| 96 | public void surfaceDestroyed(SurfaceHolder holder) { | ||
| 97 | Log.v("SDL", "surfaceDestroyed()"); | ||
| 98 | |||
| 99 | // Transition to pause, if needed | ||
| 100 | SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED; | ||
| 101 | SDLActivity.handleNativeState(); | ||
| 102 | |||
| 103 | mIsSurfaceReady = false; | ||
| 104 | SDLActivity.onNativeSurfaceDestroyed(); | ||
| 105 | } | ||
| 106 | |||
| 107 | // Called when the surface is resized | ||
| 108 | @Override | ||
| 109 | public void surfaceChanged(SurfaceHolder holder, | ||
| 110 | int format, int width, int height) { | ||
| 111 | Log.v("SDL", "surfaceChanged()"); | ||
| 112 | |||
| 113 | if (SDLActivity.mSingleton == null) { | ||
| 114 | return; | ||
| 115 | } | ||
| 116 | |||
| 117 | mWidth = width; | ||
| 118 | mHeight = height; | ||
| 119 | int nDeviceWidth = width; | ||
| 120 | int nDeviceHeight = height; | ||
| 121 | float density = 1.0f; | ||
| 122 | try | ||
| 123 | { | ||
| 124 | if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) { | ||
| 125 | DisplayMetrics realMetrics = new DisplayMetrics(); | ||
| 126 | mDisplay.getRealMetrics( realMetrics ); | ||
| 127 | nDeviceWidth = realMetrics.widthPixels; | ||
| 128 | nDeviceHeight = realMetrics.heightPixels; | ||
| 129 | // Use densityDpi instead of density to more closely match what the UI scale is | ||
| 130 | density = (float)realMetrics.densityDpi / 160.0f; | ||
| 131 | } | ||
| 132 | } catch(Exception ignored) { | ||
| 133 | } | ||
| 134 | |||
| 135 | synchronized(SDLActivity.getContext()) { | ||
| 136 | // In case we're waiting on a size change after going fullscreen, send a notification. | ||
| 137 | SDLActivity.getContext().notifyAll(); | ||
| 138 | } | ||
| 139 | |||
| 140 | Log.v("SDL", "Window size: " + width + "x" + height); | ||
| 141 | Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight); | ||
| 142 | SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, density, mDisplay.getRefreshRate()); | ||
| 143 | SDLActivity.onNativeResize(); | ||
| 144 | |||
| 145 | // Prevent a screen distortion glitch, | ||
| 146 | // for instance when the device is in Landscape and a Portrait App is resumed. | ||
| 147 | boolean skip = false; | ||
| 148 | int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation(); | ||
| 149 | |||
| 150 | if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) { | ||
| 151 | if (mWidth > mHeight) { | ||
| 152 | skip = true; | ||
| 153 | } | ||
| 154 | } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) { | ||
| 155 | if (mWidth < mHeight) { | ||
| 156 | skip = true; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | // Special Patch for Square Resolution: Black Berry Passport | ||
| 161 | if (skip) { | ||
| 162 | double min = Math.min(mWidth, mHeight); | ||
| 163 | double max = Math.max(mWidth, mHeight); | ||
| 164 | |||
| 165 | if (max / min < 1.20) { | ||
| 166 | Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution."); | ||
| 167 | skip = false; | ||
| 168 | } | ||
| 169 | } | ||
| 170 | |||
| 171 | // Don't skip if we might be multi-window or have popup dialogs | ||
| 172 | if (skip) { | ||
| 173 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
| 174 | skip = false; | ||
| 175 | } | ||
| 176 | } | ||
| 177 | |||
| 178 | if (skip) { | ||
| 179 | Log.v("SDL", "Skip .. Surface is not ready."); | ||
| 180 | mIsSurfaceReady = false; | ||
| 181 | return; | ||
| 182 | } | ||
| 183 | |||
| 184 | /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */ | ||
| 185 | SDLActivity.onNativeSurfaceChanged(); | ||
| 186 | |||
| 187 | /* Surface is ready */ | ||
| 188 | mIsSurfaceReady = true; | ||
| 189 | |||
| 190 | SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED; | ||
| 191 | SDLActivity.handleNativeState(); | ||
| 192 | } | ||
| 193 | |||
| 194 | // Window inset | ||
| 195 | @Override | ||
| 196 | public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { | ||
| 197 | if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) { | ||
| 198 | Insets combined = insets.getInsets(WindowInsets.Type.systemBars() | | ||
| 199 | WindowInsets.Type.systemGestures() | | ||
| 200 | WindowInsets.Type.mandatorySystemGestures() | | ||
| 201 | WindowInsets.Type.tappableElement() | | ||
| 202 | WindowInsets.Type.displayCutout()); | ||
| 203 | |||
| 204 | SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom); | ||
| 205 | } | ||
| 206 | |||
| 207 | // Pass these to any child views in case they need them | ||
| 208 | return insets; | ||
| 209 | } | ||
| 210 | |||
| 211 | // Key events | ||
| 212 | @Override | ||
| 213 | public boolean onKey(View v, int keyCode, KeyEvent event) { | ||
| 214 | return SDLActivity.handleKeyEvent(v, keyCode, event, null); | ||
| 215 | } | ||
| 216 | |||
| 217 | private float getNormalizedX(float x) | ||
| 218 | { | ||
| 219 | if (mWidth <= 1) { | ||
| 220 | return 0.5f; | ||
| 221 | } else { | ||
| 222 | return (x / (mWidth - 1)); | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | private float getNormalizedY(float y) | ||
| 227 | { | ||
| 228 | if (mHeight <= 1) { | ||
| 229 | return 0.5f; | ||
| 230 | } else { | ||
| 231 | return (y / (mHeight - 1)); | ||
| 232 | } | ||
| 233 | } | ||
| 234 | |||
| 235 | // Touch events | ||
| 236 | @Override | ||
| 237 | public boolean onTouch(View v, MotionEvent event) { | ||
| 238 | /* Ref: http://developer.android.com/training/gestures/multi.html */ | ||
| 239 | int touchDevId = event.getDeviceId(); | ||
| 240 | final int pointerCount = event.getPointerCount(); | ||
| 241 | int action = event.getActionMasked(); | ||
| 242 | int pointerId; | ||
| 243 | int i = 0; | ||
| 244 | float x,y,p; | ||
| 245 | |||
| 246 | if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) | ||
| 247 | i = event.getActionIndex(); | ||
| 248 | |||
| 249 | do { | ||
| 250 | int toolType = event.getToolType(i); | ||
| 251 | |||
| 252 | if (toolType == MotionEvent.TOOL_TYPE_MOUSE) { | ||
| 253 | int buttonState = event.getButtonState(); | ||
| 254 | boolean relative = false; | ||
| 255 | |||
| 256 | // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values | ||
| 257 | // if we are. We'll leverage our existing mouse motion listener | ||
| 258 | SDLGenericMotionListener_API14 motionListener = SDLActivity.getMotionListener(); | ||
| 259 | x = motionListener.getEventX(event, i); | ||
| 260 | y = motionListener.getEventY(event, i); | ||
| 261 | relative = motionListener.inRelativeMode(); | ||
| 262 | |||
| 263 | SDLActivity.onNativeMouse(buttonState, action, x, y, relative); | ||
| 264 | } else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) { | ||
| 265 | pointerId = event.getPointerId(i); | ||
| 266 | x = event.getX(i); | ||
| 267 | y = event.getY(i); | ||
| 268 | p = event.getPressure(i); | ||
| 269 | if (p > 1.0f) { | ||
| 270 | // may be larger than 1.0f on some devices | ||
| 271 | // see the documentation of getPressure(i) | ||
| 272 | p = 1.0f; | ||
| 273 | } | ||
| 274 | |||
| 275 | // BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP | ||
| 276 | int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30)); | ||
| 277 | |||
| 278 | SDLActivity.onNativePen(pointerId, buttonState, action, x, y, p); | ||
| 279 | } else { // MotionEvent.TOOL_TYPE_FINGER or MotionEvent.TOOL_TYPE_UNKNOWN | ||
| 280 | pointerId = event.getPointerId(i); | ||
| 281 | x = getNormalizedX(event.getX(i)); | ||
| 282 | y = getNormalizedY(event.getY(i)); | ||
| 283 | p = event.getPressure(i); | ||
| 284 | if (p > 1.0f) { | ||
| 285 | // may be larger than 1.0f on some devices | ||
| 286 | // see the documentation of getPressure(i) | ||
| 287 | p = 1.0f; | ||
| 288 | } | ||
| 289 | |||
| 290 | SDLActivity.onNativeTouch(touchDevId, pointerId, action, x, y, p); | ||
| 291 | } | ||
| 292 | |||
| 293 | // Non-primary up/down | ||
| 294 | if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) | ||
| 295 | break; | ||
| 296 | } while (++i < pointerCount); | ||
| 297 | |||
| 298 | return true; | ||
| 299 | } | ||
| 300 | |||
| 301 | // Sensor events | ||
| 302 | public void enableSensor(int sensortype, boolean enabled) { | ||
| 303 | // TODO: This uses getDefaultSensor - what if we have >1 accels? | ||
| 304 | if (enabled) { | ||
| 305 | mSensorManager.registerListener(this, | ||
| 306 | mSensorManager.getDefaultSensor(sensortype), | ||
| 307 | SensorManager.SENSOR_DELAY_GAME, null); | ||
| 308 | } else { | ||
| 309 | mSensorManager.unregisterListener(this, | ||
| 310 | mSensorManager.getDefaultSensor(sensortype)); | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | @Override | ||
| 315 | public void onAccuracyChanged(Sensor sensor, int accuracy) { | ||
| 316 | // TODO | ||
| 317 | } | ||
| 318 | |||
| 319 | @Override | ||
| 320 | public void onSensorChanged(SensorEvent event) { | ||
| 321 | if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { | ||
| 322 | |||
| 323 | // Since we may have an orientation set, we won't receive onConfigurationChanged events. | ||
| 324 | // We thus should check here. | ||
| 325 | int newRotation; | ||
| 326 | |||
| 327 | float x, y; | ||
| 328 | switch (mDisplay.getRotation()) { | ||
| 329 | case Surface.ROTATION_0: | ||
| 330 | default: | ||
| 331 | x = event.values[0]; | ||
| 332 | y = event.values[1]; | ||
| 333 | newRotation = 0; | ||
| 334 | break; | ||
| 335 | case Surface.ROTATION_90: | ||
| 336 | x = -event.values[1]; | ||
| 337 | y = event.values[0]; | ||
| 338 | newRotation = 90; | ||
| 339 | break; | ||
| 340 | case Surface.ROTATION_180: | ||
| 341 | x = -event.values[0]; | ||
| 342 | y = -event.values[1]; | ||
| 343 | newRotation = 180; | ||
| 344 | break; | ||
| 345 | case Surface.ROTATION_270: | ||
| 346 | x = event.values[1]; | ||
| 347 | y = -event.values[0]; | ||
| 348 | newRotation = 270; | ||
| 349 | break; | ||
| 350 | } | ||
| 351 | |||
| 352 | if (newRotation != SDLActivity.mCurrentRotation) { | ||
| 353 | SDLActivity.mCurrentRotation = newRotation; | ||
| 354 | SDLActivity.onNativeRotationChanged(newRotation); | ||
| 355 | } | ||
| 356 | |||
| 357 | SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH, | ||
| 358 | y / SensorManager.GRAVITY_EARTH, | ||
| 359 | event.values[2] / SensorManager.GRAVITY_EARTH); | ||
| 360 | |||
| 361 | |||
| 362 | } | ||
| 363 | } | ||
| 364 | |||
| 365 | // Captured pointer events for API 26. | ||
| 366 | public boolean onCapturedPointerEvent(MotionEvent event) | ||
| 367 | { | ||
| 368 | int action = event.getActionMasked(); | ||
| 369 | int pointerCount = event.getPointerCount(); | ||
| 370 | |||
| 371 | for (int i = 0; i < pointerCount; i++) { | ||
| 372 | float x, y; | ||
| 373 | switch (action) { | ||
| 374 | case MotionEvent.ACTION_SCROLL: | ||
| 375 | x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i); | ||
| 376 | y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i); | ||
| 377 | SDLActivity.onNativeMouse(0, action, x, y, false); | ||
| 378 | return true; | ||
| 379 | |||
| 380 | case MotionEvent.ACTION_HOVER_MOVE: | ||
| 381 | case MotionEvent.ACTION_MOVE: | ||
| 382 | x = event.getX(i); | ||
| 383 | y = event.getY(i); | ||
| 384 | SDLActivity.onNativeMouse(0, action, x, y, true); | ||
| 385 | return true; | ||
| 386 | |||
| 387 | case MotionEvent.ACTION_BUTTON_PRESS: | ||
| 388 | case MotionEvent.ACTION_BUTTON_RELEASE: | ||
| 389 | |||
| 390 | // Change our action value to what SDL's code expects. | ||
| 391 | if (action == MotionEvent.ACTION_BUTTON_PRESS) { | ||
| 392 | action = MotionEvent.ACTION_DOWN; | ||
| 393 | } else { /* MotionEvent.ACTION_BUTTON_RELEASE */ | ||
| 394 | action = MotionEvent.ACTION_UP; | ||
| 395 | } | ||
| 396 | |||
| 397 | x = event.getX(i); | ||
| 398 | y = event.getY(i); | ||
| 399 | int button = event.getButtonState(); | ||
| 400 | |||
| 401 | SDLActivity.onNativeMouse(button, action, x, y, true); | ||
| 402 | return true; | ||
| 403 | } | ||
| 404 | } | ||
| 405 | |||
| 406 | return false; | ||
| 407 | } | ||
| 408 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..d50bdaa --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png | |||
| Binary files differ | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..0a299eb --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png | |||
| Binary files differ | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..a336ad5 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png | |||
| Binary files differ | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d423dac --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png | |||
| Binary files differ | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..959c384 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png | |||
| Binary files differ | |||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/res/values/colors.xml b/contrib/SDL-3.2.8/android-project/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/res/values/colors.xml | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <resources> | ||
| 3 | <color name="colorPrimary">#3F51B5</color> | ||
| 4 | <color name="colorPrimaryDark">#303F9F</color> | ||
| 5 | <color name="colorAccent">#FF4081</color> | ||
| 6 | </resources> | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/res/values/strings.xml b/contrib/SDL-3.2.8/android-project/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..ab79533 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/res/values/strings.xml | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | <resources> | ||
| 2 | <string name="app_name">Game</string> | ||
| 3 | </resources> | ||
diff --git a/contrib/SDL-3.2.8/android-project/app/src/main/res/values/styles.xml b/contrib/SDL-3.2.8/android-project/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..7456b1b --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/app/src/main/res/values/styles.xml | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <resources> | ||
| 3 | <!-- Base application theme. --> | ||
| 4 | <style name="AppTheme" parent="android:Theme.NoTitleBar.Fullscreen"> | ||
| 5 | <!-- Customize your theme here. --> | ||
| 6 | </style> | ||
| 7 | </resources> | ||
diff --git a/contrib/SDL-3.2.8/android-project/build.gradle b/contrib/SDL-3.2.8/android-project/build.gradle new file mode 100644 index 0000000..ed2299c --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/build.gradle | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. | ||
| 2 | |||
| 3 | buildscript { | ||
| 4 | repositories { | ||
| 5 | mavenCentral() | ||
| 6 | google() | ||
| 7 | } | ||
| 8 | dependencies { | ||
| 9 | classpath 'com.android.tools.build:gradle:8.7.3' | ||
| 10 | |||
| 11 | // NOTE: Do not place your application dependencies here; they belong | ||
| 12 | // in the individual module build.gradle files | ||
| 13 | } | ||
| 14 | } | ||
| 15 | |||
| 16 | allprojects { | ||
| 17 | repositories { | ||
| 18 | mavenCentral() | ||
| 19 | google() | ||
| 20 | } | ||
| 21 | } | ||
| 22 | |||
| 23 | task clean(type: Delete) { | ||
| 24 | delete rootProject.buildDir | ||
| 25 | } | ||
diff --git a/contrib/SDL-3.2.8/android-project/gradle.properties b/contrib/SDL-3.2.8/android-project/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/gradle.properties | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | # Project-wide Gradle settings. | ||
| 2 | |||
| 3 | # IDE (e.g. Android Studio) users: | ||
| 4 | # Gradle settings configured through the IDE *will override* | ||
| 5 | # any settings specified in this file. | ||
| 6 | |||
| 7 | # For more details on how to configure your build environment visit | ||
| 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html | ||
| 9 | |||
| 10 | # Specifies the JVM arguments used for the daemon process. | ||
| 11 | # The setting is particularly useful for tweaking memory settings. | ||
| 12 | org.gradle.jvmargs=-Xmx1536m | ||
| 13 | |||
| 14 | # When configured, Gradle will run in incubating parallel mode. | ||
| 15 | # This option should only be used with decoupled projects. More details, visit | ||
| 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects | ||
| 17 | # org.gradle.parallel=true | ||
diff --git a/contrib/SDL-3.2.8/android-project/gradle/wrapper/gradle-wrapper.jar b/contrib/SDL-3.2.8/android-project/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..2b338a9 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/gradle/wrapper/gradle-wrapper.jar | |||
| Binary files differ | |||
diff --git a/contrib/SDL-3.2.8/android-project/gradle/wrapper/gradle-wrapper.properties b/contrib/SDL-3.2.8/android-project/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..99fbfa0 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/gradle/wrapper/gradle-wrapper.properties | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | #Thu Nov 11 18:20:34 PST 2021 | ||
| 2 | distributionBase=GRADLE_USER_HOME | ||
| 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip | ||
| 4 | distributionPath=wrapper/dists | ||
| 5 | zipStorePath=wrapper/dists | ||
| 6 | zipStoreBase=GRADLE_USER_HOME | ||
diff --git a/contrib/SDL-3.2.8/android-project/gradlew b/contrib/SDL-3.2.8/android-project/gradlew new file mode 100755 index 0000000..3427607 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/gradlew | |||
| @@ -0,0 +1,160 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | |||
| 3 | ############################################################################## | ||
| 4 | ## | ||
| 5 | ## Gradle start up script for UN*X | ||
| 6 | ## | ||
| 7 | ############################################################################## | ||
| 8 | |||
| 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||
| 10 | DEFAULT_JVM_OPTS="" | ||
| 11 | |||
| 12 | APP_NAME="Gradle" | ||
| 13 | APP_BASE_NAME=`basename "$0"` | ||
| 14 | |||
| 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. | ||
| 16 | MAX_FD="maximum" | ||
| 17 | |||
| 18 | warn ( ) { | ||
| 19 | echo "$*" | ||
| 20 | } | ||
| 21 | |||
| 22 | die ( ) { | ||
| 23 | echo | ||
| 24 | echo "$*" | ||
| 25 | echo | ||
| 26 | exit 1 | ||
| 27 | } | ||
| 28 | |||
| 29 | # OS specific support (must be 'true' or 'false'). | ||
| 30 | cygwin=false | ||
| 31 | msys=false | ||
| 32 | darwin=false | ||
| 33 | case "`uname`" in | ||
| 34 | CYGWIN* ) | ||
| 35 | cygwin=true | ||
| 36 | ;; | ||
| 37 | Darwin* ) | ||
| 38 | darwin=true | ||
| 39 | ;; | ||
| 40 | MINGW* ) | ||
| 41 | msys=true | ||
| 42 | ;; | ||
| 43 | esac | ||
| 44 | |||
| 45 | # Attempt to set APP_HOME | ||
| 46 | # Resolve links: $0 may be a link | ||
| 47 | PRG="$0" | ||
| 48 | # Need this for relative symlinks. | ||
| 49 | while [ -h "$PRG" ] ; do | ||
| 50 | ls=`ls -ld "$PRG"` | ||
| 51 | link=`expr "$ls" : '.*-> \(.*\)$'` | ||
| 52 | if expr "$link" : '/.*' > /dev/null; then | ||
| 53 | PRG="$link" | ||
| 54 | else | ||
| 55 | PRG=`dirname "$PRG"`"/$link" | ||
| 56 | fi | ||
| 57 | done | ||
| 58 | SAVED="`pwd`" | ||
| 59 | cd "`dirname \"$PRG\"`/" >/dev/null | ||
| 60 | APP_HOME="`pwd -P`" | ||
| 61 | cd "$SAVED" >/dev/null | ||
| 62 | |||
| 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||
| 64 | |||
| 65 | # Determine the Java command to use to start the JVM. | ||
| 66 | if [ -n "$JAVA_HOME" ] ; then | ||
| 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | ||
| 68 | # IBM's JDK on AIX uses strange locations for the executables | ||
| 69 | JAVACMD="$JAVA_HOME/jre/sh/java" | ||
| 70 | else | ||
| 71 | JAVACMD="$JAVA_HOME/bin/java" | ||
| 72 | fi | ||
| 73 | if [ ! -x "$JAVACMD" ] ; then | ||
| 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME | ||
| 75 | |||
| 76 | Please set the JAVA_HOME variable in your environment to match the | ||
| 77 | location of your Java installation." | ||
| 78 | fi | ||
| 79 | else | ||
| 80 | JAVACMD="java" | ||
| 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||
| 82 | |||
| 83 | Please set the JAVA_HOME variable in your environment to match the | ||
| 84 | location of your Java installation." | ||
| 85 | fi | ||
| 86 | |||
| 87 | # Increase the maximum file descriptors if we can. | ||
| 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then | ||
| 89 | MAX_FD_LIMIT=`ulimit -H -n` | ||
| 90 | if [ $? -eq 0 ] ; then | ||
| 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then | ||
| 92 | MAX_FD="$MAX_FD_LIMIT" | ||
| 93 | fi | ||
| 94 | ulimit -n $MAX_FD | ||
| 95 | if [ $? -ne 0 ] ; then | ||
| 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" | ||
| 97 | fi | ||
| 98 | else | ||
| 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" | ||
| 100 | fi | ||
| 101 | fi | ||
| 102 | |||
| 103 | # For Darwin, add options to specify how the application appears in the dock | ||
| 104 | if $darwin; then | ||
| 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" | ||
| 106 | fi | ||
| 107 | |||
| 108 | # For Cygwin, switch paths to Windows format before running java | ||
| 109 | if $cygwin ; then | ||
| 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` | ||
| 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` | ||
| 112 | JAVACMD=`cygpath --unix "$JAVACMD"` | ||
| 113 | |||
| 114 | # We build the pattern for arguments to be converted via cygpath | ||
| 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` | ||
| 116 | SEP="" | ||
| 117 | for dir in $ROOTDIRSRAW ; do | ||
| 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" | ||
| 119 | SEP="|" | ||
| 120 | done | ||
| 121 | OURCYGPATTERN="(^($ROOTDIRS))" | ||
| 122 | # Add a user-defined pattern to the cygpath arguments | ||
| 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then | ||
| 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" | ||
| 125 | fi | ||
| 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh | ||
| 127 | i=0 | ||
| 128 | for arg in "$@" ; do | ||
| 129 | CHECK=`echo "$arg"|grep -E -c "$OURCYGPATTERN" -` | ||
| 130 | CHECK2=`echo "$arg"|grep -E -c "^-"` ### Determine if an option | ||
| 131 | |||
| 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition | ||
| 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` | ||
| 134 | else | ||
| 135 | eval `echo args$i`="\"$arg\"" | ||
| 136 | fi | ||
| 137 | i=$((i+1)) | ||
| 138 | done | ||
| 139 | case $i in | ||
| 140 | (0) set -- ;; | ||
| 141 | (1) set -- "$args0" ;; | ||
| 142 | (2) set -- "$args0" "$args1" ;; | ||
| 143 | (3) set -- "$args0" "$args1" "$args2" ;; | ||
| 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; | ||
| 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; | ||
| 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; | ||
| 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; | ||
| 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; | ||
| 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; | ||
| 150 | esac | ||
| 151 | fi | ||
| 152 | |||
| 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules | ||
| 154 | function splitJvmOpts() { | ||
| 155 | JVM_OPTS=("$@") | ||
| 156 | } | ||
| 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS | ||
| 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" | ||
| 159 | |||
| 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" | ||
diff --git a/contrib/SDL-3.2.8/android-project/gradlew.bat b/contrib/SDL-3.2.8/android-project/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/gradlew.bat | |||
| @@ -0,0 +1,90 @@ | |||
| 1 | @if "%DEBUG%" == "" @echo off | ||
| 2 | @rem ########################################################################## | ||
| 3 | @rem | ||
| 4 | @rem Gradle startup script for Windows | ||
| 5 | @rem | ||
| 6 | @rem ########################################################################## | ||
| 7 | |||
| 8 | @rem Set local scope for the variables with windows NT shell | ||
| 9 | if "%OS%"=="Windows_NT" setlocal | ||
| 10 | |||
| 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||
| 12 | set DEFAULT_JVM_OPTS= | ||
| 13 | |||
| 14 | set DIRNAME=%~dp0 | ||
| 15 | if "%DIRNAME%" == "" set DIRNAME=. | ||
| 16 | set APP_BASE_NAME=%~n0 | ||
| 17 | set APP_HOME=%DIRNAME% | ||
| 18 | |||
| 19 | @rem Find java.exe | ||
| 20 | if defined JAVA_HOME goto findJavaFromJavaHome | ||
| 21 | |||
| 22 | set JAVA_EXE=java.exe | ||
| 23 | %JAVA_EXE% -version >NUL 2>&1 | ||
| 24 | if "%ERRORLEVEL%" == "0" goto init | ||
| 25 | |||
| 26 | echo. | ||
| 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||
| 28 | echo. | ||
| 29 | echo Please set the JAVA_HOME variable in your environment to match the | ||
| 30 | echo location of your Java installation. | ||
| 31 | |||
| 32 | goto fail | ||
| 33 | |||
| 34 | :findJavaFromJavaHome | ||
| 35 | set JAVA_HOME=%JAVA_HOME:"=% | ||
| 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe | ||
| 37 | |||
| 38 | if exist "%JAVA_EXE%" goto init | ||
| 39 | |||
| 40 | echo. | ||
| 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% | ||
| 42 | echo. | ||
| 43 | echo Please set the JAVA_HOME variable in your environment to match the | ||
| 44 | echo location of your Java installation. | ||
| 45 | |||
| 46 | goto fail | ||
| 47 | |||
| 48 | :init | ||
| 49 | @rem Get command-line arguments, handling Windowz variants | ||
| 50 | |||
| 51 | if not "%OS%" == "Windows_NT" goto win9xME_args | ||
| 52 | if "%@eval[2+2]" == "4" goto 4NT_args | ||
| 53 | |||
| 54 | :win9xME_args | ||
| 55 | @rem Slurp the command line arguments. | ||
| 56 | set CMD_LINE_ARGS= | ||
| 57 | set _SKIP=2 | ||
| 58 | |||
| 59 | :win9xME_args_slurp | ||
| 60 | if "x%~1" == "x" goto execute | ||
| 61 | |||
| 62 | set CMD_LINE_ARGS=%* | ||
| 63 | goto execute | ||
| 64 | |||
| 65 | :4NT_args | ||
| 66 | @rem Get arguments from the 4NT Shell from JP Software | ||
| 67 | set CMD_LINE_ARGS=%$ | ||
| 68 | |||
| 69 | :execute | ||
| 70 | @rem Setup the command line | ||
| 71 | |||
| 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||
| 73 | |||
| 74 | @rem Execute Gradle | ||
| 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | ||
| 76 | |||
| 77 | :end | ||
| 78 | @rem End local scope for the variables with windows NT shell | ||
| 79 | if "%ERRORLEVEL%"=="0" goto mainEnd | ||
| 80 | |||
| 81 | :fail | ||
| 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||
| 83 | rem the _cmd.exe /c_ return code! | ||
| 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 | ||
| 85 | exit /b 1 | ||
| 86 | |||
| 87 | :mainEnd | ||
| 88 | if "%OS%"=="Windows_NT" endlocal | ||
| 89 | |||
| 90 | :omega | ||
diff --git a/contrib/SDL-3.2.8/android-project/settings.gradle b/contrib/SDL-3.2.8/android-project/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/contrib/SDL-3.2.8/android-project/settings.gradle | |||
| @@ -0,0 +1 @@ | |||
| include ':app' | |||
