diff options
author | 3gg <3gg@shellblade.net> | 2025-08-30 16:53:58 -0700 |
---|---|---|
committer | 3gg <3gg@shellblade.net> | 2025-08-30 16:53:58 -0700 |
commit | 6aaedb813fa11ba0679c3051bc2eb28646b9506c (patch) | |
tree | 34acbfc9840e02cb4753e6306ea7ce978bf8b58e /src/contrib/SDL-3.2.20/android-project | |
parent | 8f228ade99dd3d4c8da9b78ade1815c9adf85c8f (diff) |
Update to SDL3
Diffstat (limited to 'src/contrib/SDL-3.2.20/android-project')
35 files changed, 6248 insertions, 0 deletions
diff --git a/src/contrib/SDL-3.2.20/android-project/app/build.gradle b/src/contrib/SDL-3.2.20/android-project/app/build.gradle new file mode 100644 index 0000000..f44cf26 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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-21" | ||
18 | // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' | ||
19 | abiFilters 'arm64-v8a' | ||
20 | } | ||
21 | cmake { | ||
22 | arguments "-DANDROID_PLATFORM=android-21", "-DANDROID_STL=c++_static", "-DAPP_SUPPORT_FLEXIBLE_PAGE_SIZES=true" | ||
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/src/contrib/SDL-3.2.20/android-project/app/jni/Android.mk b/src/contrib/SDL-3.2.20/android-project/app/jni/Android.mk new file mode 100644 index 0000000..5053e7d --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/jni/Android.mk | |||
@@ -0,0 +1 @@ | |||
include $(call all-subdir-makefiles) | |||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/jni/Application.mk b/src/contrib/SDL-3.2.20/android-project/app/jni/Application.mk new file mode 100644 index 0000000..80b73fd --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/jni/Application.mk | |||
@@ -0,0 +1,13 @@ | |||
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-21 | ||
11 | |||
12 | # https://developer.android.com/guide/practices/page-sizes#update-packaging | ||
13 | APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true \ No newline at end of file | ||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/jni/CMakeLists.txt b/src/contrib/SDL-3.2.20/android-project/app/jni/CMakeLists.txt new file mode 100644 index 0000000..404b87b --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/jni/src/Android.mk b/src/contrib/SDL-3.2.20/android-project/app/jni/src/Android.mk new file mode 100644 index 0000000..61672d4 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/jni/src/CMakeLists.txt b/src/contrib/SDL-3.2.20/android-project/app/jni/src/CMakeLists.txt new file mode 100644 index 0000000..df0a4d0 --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/jni/src/CMakeLists.txt | |||
@@ -0,0 +1,34 @@ | |||
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 | |||
30 | #https://developer.android.com/guide/practices/page-sizes#update-packaging | ||
31 | target_link_options(main PRIVATE "-Wl,-z,max-page-size=16384") | ||
32 | target_link_options(main PRIVATE "-Wl,-z,common-page-size=16384") | ||
33 | |||
34 | target_link_libraries(main PRIVATE SDL3::SDL3) | ||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/jni/src/YourSourceHere.c b/src/contrib/SDL-3.2.20/android-project/app/jni/src/YourSourceHere.c new file mode 100644 index 0000000..87b8297 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/proguard-rules.pro b/src/contrib/SDL-3.2.20/android-project/app/proguard-rules.pro new file mode 100644 index 0000000..5f8ee6a --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/proguard-rules.pro | |||
@@ -0,0 +1,78 @@ | |||
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 | java.lang.String getPreferredLocales(); | ||
55 | java.lang.String formatLocale(java.util.Locale); | ||
56 | } | ||
57 | |||
58 | -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.HIDDeviceManager { | ||
59 | void closeDevice(int); | ||
60 | boolean initialize(boolean, boolean); | ||
61 | boolean openDevice(int); | ||
62 | boolean readReport(int, byte[], boolean); | ||
63 | int writeReport(int, byte[], boolean); | ||
64 | } | ||
65 | |||
66 | -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLAudioManager { | ||
67 | void registerAudioDeviceCallback(); | ||
68 | void unregisterAudioDeviceCallback(); | ||
69 | void audioSetThreadPriority(boolean, int); | ||
70 | } | ||
71 | |||
72 | -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLControllerManager { | ||
73 | void pollInputDevices(); | ||
74 | void pollHapticDevices(); | ||
75 | void hapticRun(int, float, int); | ||
76 | void hapticRumble(int, float, float, int); | ||
77 | void hapticStop(int); | ||
78 | } | ||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/src/main/AndroidManifest.xml b/src/contrib/SDL-3.2.20/android-project/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f3a7cd5 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java new file mode 100644 index 0000000..f960953 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java new file mode 100644 index 0000000..d2dc0d2 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java new file mode 100644 index 0000000..37d80ca --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java new file mode 100644 index 0000000..2741438 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDL.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDL.java new file mode 100644 index 0000000..b132fea --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java new file mode 100644 index 0000000..0aa1ef3 --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java | |||
@@ -0,0 +1,2228 @@ | |||
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.LocaleList; | ||
27 | import android.os.Message; | ||
28 | import android.os.ParcelFileDescriptor; | ||
29 | import android.util.DisplayMetrics; | ||
30 | import android.util.Log; | ||
31 | import android.util.SparseArray; | ||
32 | import android.view.Display; | ||
33 | import android.view.Gravity; | ||
34 | import android.view.InputDevice; | ||
35 | import android.view.KeyEvent; | ||
36 | import android.view.PointerIcon; | ||
37 | import android.view.Surface; | ||
38 | import android.view.View; | ||
39 | import android.view.ViewGroup; | ||
40 | import android.view.Window; | ||
41 | import android.view.WindowManager; | ||
42 | import android.view.inputmethod.InputConnection; | ||
43 | import android.view.inputmethod.InputMethodManager; | ||
44 | import android.webkit.MimeTypeMap; | ||
45 | import android.widget.Button; | ||
46 | import android.widget.LinearLayout; | ||
47 | import android.widget.RelativeLayout; | ||
48 | import android.widget.TextView; | ||
49 | import android.widget.Toast; | ||
50 | |||
51 | import java.io.FileNotFoundException; | ||
52 | import java.util.ArrayList; | ||
53 | import java.util.Hashtable; | ||
54 | import java.util.Locale; | ||
55 | |||
56 | |||
57 | /** | ||
58 | SDL Activity | ||
59 | */ | ||
60 | public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { | ||
61 | private static final String TAG = "SDL"; | ||
62 | private static final int SDL_MAJOR_VERSION = 3; | ||
63 | private static final int SDL_MINOR_VERSION = 2; | ||
64 | private static final int SDL_MICRO_VERSION = 20; | ||
65 | /* | ||
66 | // Display InputType.SOURCE/CLASS of events and devices | ||
67 | // | ||
68 | // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]"); | ||
69 | // SDLActivity.debugSource(event.getSource(), "event"); | ||
70 | public static void debugSource(int sources, String prefix) { | ||
71 | int s = sources; | ||
72 | int s_copy = sources; | ||
73 | String cls = ""; | ||
74 | String src = ""; | ||
75 | int tst = 0; | ||
76 | int FLAG_TAINTED = 0x80000000; | ||
77 | |||
78 | if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0) cls += " BUTTON"; | ||
79 | if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) cls += " JOYSTICK"; | ||
80 | if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0) cls += " POINTER"; | ||
81 | if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0) cls += " POSITION"; | ||
82 | if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) cls += " TRACKBALL"; | ||
83 | |||
84 | |||
85 | int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits | ||
86 | s2 &= ~( InputDevice.SOURCE_CLASS_BUTTON | ||
87 | | InputDevice.SOURCE_CLASS_JOYSTICK | ||
88 | | InputDevice.SOURCE_CLASS_POINTER | ||
89 | | InputDevice.SOURCE_CLASS_POSITION | ||
90 | | InputDevice.SOURCE_CLASS_TRACKBALL); | ||
91 | |||
92 | if (s2 != 0) cls += "Some_Unknown"; | ||
93 | |||
94 | s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; | ||
95 | |||
96 | if (Build.VERSION.SDK_INT >= 23) { | ||
97 | tst = InputDevice.SOURCE_BLUETOOTH_STYLUS; | ||
98 | if ((s & tst) == tst) src += " BLUETOOTH_STYLUS"; | ||
99 | s2 &= ~tst; | ||
100 | } | ||
101 | |||
102 | tst = InputDevice.SOURCE_DPAD; | ||
103 | if ((s & tst) == tst) src += " DPAD"; | ||
104 | s2 &= ~tst; | ||
105 | |||
106 | tst = InputDevice.SOURCE_GAMEPAD; | ||
107 | if ((s & tst) == tst) src += " GAMEPAD"; | ||
108 | s2 &= ~tst; | ||
109 | |||
110 | if (Build.VERSION.SDK_INT >= 21) { | ||
111 | tst = InputDevice.SOURCE_HDMI; | ||
112 | if ((s & tst) == tst) src += " HDMI"; | ||
113 | s2 &= ~tst; | ||
114 | } | ||
115 | |||
116 | tst = InputDevice.SOURCE_JOYSTICK; | ||
117 | if ((s & tst) == tst) src += " JOYSTICK"; | ||
118 | s2 &= ~tst; | ||
119 | |||
120 | tst = InputDevice.SOURCE_KEYBOARD; | ||
121 | if ((s & tst) == tst) src += " KEYBOARD"; | ||
122 | s2 &= ~tst; | ||
123 | |||
124 | tst = InputDevice.SOURCE_MOUSE; | ||
125 | if ((s & tst) == tst) src += " MOUSE"; | ||
126 | s2 &= ~tst; | ||
127 | |||
128 | if (Build.VERSION.SDK_INT >= 26) { | ||
129 | tst = InputDevice.SOURCE_MOUSE_RELATIVE; | ||
130 | if ((s & tst) == tst) src += " MOUSE_RELATIVE"; | ||
131 | s2 &= ~tst; | ||
132 | |||
133 | tst = InputDevice.SOURCE_ROTARY_ENCODER; | ||
134 | if ((s & tst) == tst) src += " ROTARY_ENCODER"; | ||
135 | s2 &= ~tst; | ||
136 | } | ||
137 | tst = InputDevice.SOURCE_STYLUS; | ||
138 | if ((s & tst) == tst) src += " STYLUS"; | ||
139 | s2 &= ~tst; | ||
140 | |||
141 | tst = InputDevice.SOURCE_TOUCHPAD; | ||
142 | if ((s & tst) == tst) src += " TOUCHPAD"; | ||
143 | s2 &= ~tst; | ||
144 | |||
145 | tst = InputDevice.SOURCE_TOUCHSCREEN; | ||
146 | if ((s & tst) == tst) src += " TOUCHSCREEN"; | ||
147 | s2 &= ~tst; | ||
148 | |||
149 | if (Build.VERSION.SDK_INT >= 18) { | ||
150 | tst = InputDevice.SOURCE_TOUCH_NAVIGATION; | ||
151 | if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; | ||
152 | s2 &= ~tst; | ||
153 | } | ||
154 | |||
155 | tst = InputDevice.SOURCE_TRACKBALL; | ||
156 | if ((s & tst) == tst) src += " TRACKBALL"; | ||
157 | s2 &= ~tst; | ||
158 | |||
159 | tst = InputDevice.SOURCE_ANY; | ||
160 | if ((s & tst) == tst) src += " ANY"; | ||
161 | s2 &= ~tst; | ||
162 | |||
163 | if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; | ||
164 | s2 &= ~FLAG_TAINTED; | ||
165 | |||
166 | if (s2 != 0) src += " Some_Unknown"; | ||
167 | |||
168 | Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); | ||
169 | } | ||
170 | */ | ||
171 | |||
172 | public static boolean mIsResumedCalled, mHasFocus; | ||
173 | public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */); | ||
174 | |||
175 | // Cursor types | ||
176 | // private static final int SDL_SYSTEM_CURSOR_NONE = -1; | ||
177 | private static final int SDL_SYSTEM_CURSOR_ARROW = 0; | ||
178 | private static final int SDL_SYSTEM_CURSOR_IBEAM = 1; | ||
179 | private static final int SDL_SYSTEM_CURSOR_WAIT = 2; | ||
180 | private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3; | ||
181 | private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4; | ||
182 | private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5; | ||
183 | private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6; | ||
184 | private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7; | ||
185 | private static final int SDL_SYSTEM_CURSOR_SIZENS = 8; | ||
186 | private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; | ||
187 | private static final int SDL_SYSTEM_CURSOR_NO = 10; | ||
188 | private static final int SDL_SYSTEM_CURSOR_HAND = 11; | ||
189 | private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT = 12; | ||
190 | private static final int SDL_SYSTEM_CURSOR_WINDOW_TOP = 13; | ||
191 | private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT = 14; | ||
192 | private static final int SDL_SYSTEM_CURSOR_WINDOW_RIGHT = 15; | ||
193 | private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT = 16; | ||
194 | private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOM = 17; | ||
195 | private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT = 18; | ||
196 | private static final int SDL_SYSTEM_CURSOR_WINDOW_LEFT = 19; | ||
197 | |||
198 | protected static final int SDL_ORIENTATION_UNKNOWN = 0; | ||
199 | protected static final int SDL_ORIENTATION_LANDSCAPE = 1; | ||
200 | protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2; | ||
201 | protected static final int SDL_ORIENTATION_PORTRAIT = 3; | ||
202 | protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; | ||
203 | |||
204 | protected static int mCurrentRotation; | ||
205 | protected static Locale mCurrentLocale; | ||
206 | |||
207 | // Handle the state of the native layer | ||
208 | public enum NativeState { | ||
209 | INIT, RESUMED, PAUSED | ||
210 | } | ||
211 | |||
212 | public static NativeState mNextNativeState; | ||
213 | public static NativeState mCurrentNativeState; | ||
214 | |||
215 | /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ | ||
216 | public static boolean mBrokenLibraries = true; | ||
217 | |||
218 | // Main components | ||
219 | protected static SDLActivity mSingleton; | ||
220 | protected static SDLSurface mSurface; | ||
221 | protected static SDLDummyEdit mTextEdit; | ||
222 | protected static boolean mScreenKeyboardShown; | ||
223 | protected static ViewGroup mLayout; | ||
224 | protected static SDLClipboardHandler mClipboardHandler; | ||
225 | protected static Hashtable<Integer, PointerIcon> mCursors; | ||
226 | protected static int mLastCursorID; | ||
227 | protected static SDLGenericMotionListener_API14 mMotionListener; | ||
228 | protected static HIDDeviceManager mHIDDeviceManager; | ||
229 | |||
230 | // This is what SDL runs in. It invokes SDL_main(), eventually | ||
231 | protected static Thread mSDLThread; | ||
232 | protected static boolean mSDLMainFinished = false; | ||
233 | protected static boolean mActivityCreated = false; | ||
234 | private static SDLFileDialogState mFileDialogState = null; | ||
235 | protected static boolean mDispatchingKeyEvent = false; | ||
236 | |||
237 | protected static SDLGenericMotionListener_API14 getMotionListener() { | ||
238 | if (mMotionListener == null) { | ||
239 | if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { | ||
240 | mMotionListener = new SDLGenericMotionListener_API26(); | ||
241 | } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
242 | mMotionListener = new SDLGenericMotionListener_API24(); | ||
243 | } else { | ||
244 | mMotionListener = new SDLGenericMotionListener_API14(); | ||
245 | } | ||
246 | } | ||
247 | |||
248 | return mMotionListener; | ||
249 | } | ||
250 | |||
251 | /** | ||
252 | * The application entry point, called on a dedicated thread (SDLThread). | ||
253 | * The default implementation uses the getMainSharedObject() and getMainFunction() methods | ||
254 | * to invoke native code from the specified shared library. | ||
255 | * It can be overridden by derived classes. | ||
256 | */ | ||
257 | protected void main() { | ||
258 | String library = SDLActivity.mSingleton.getMainSharedObject(); | ||
259 | String function = SDLActivity.mSingleton.getMainFunction(); | ||
260 | String[] arguments = SDLActivity.mSingleton.getArguments(); | ||
261 | |||
262 | Log.v("SDL", "Running main function " + function + " from library " + library); | ||
263 | SDLActivity.nativeRunMain(library, function, arguments); | ||
264 | Log.v("SDL", "Finished main function"); | ||
265 | } | ||
266 | |||
267 | /** | ||
268 | * This method returns the name of the shared object with the application entry point | ||
269 | * It can be overridden by derived classes. | ||
270 | */ | ||
271 | protected String getMainSharedObject() { | ||
272 | String library; | ||
273 | String[] libraries = SDLActivity.mSingleton.getLibraries(); | ||
274 | if (libraries.length > 0) { | ||
275 | library = "lib" + libraries[libraries.length - 1] + ".so"; | ||
276 | } else { | ||
277 | library = "libmain.so"; | ||
278 | } | ||
279 | return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; | ||
280 | } | ||
281 | |||
282 | /** | ||
283 | * This method returns the name of the application entry point | ||
284 | * It can be overridden by derived classes. | ||
285 | */ | ||
286 | protected String getMainFunction() { | ||
287 | return "SDL_main"; | ||
288 | } | ||
289 | |||
290 | /** | ||
291 | * This method is called by SDL before loading the native shared libraries. | ||
292 | * It can be overridden to provide names of shared libraries to be loaded. | ||
293 | * The default implementation returns the defaults. It never returns null. | ||
294 | * An array returned by a new implementation must at least contain "SDL3". | ||
295 | * Also keep in mind that the order the libraries are loaded may matter. | ||
296 | * @return names of shared libraries to be loaded (e.g. "SDL3", "main"). | ||
297 | */ | ||
298 | protected String[] getLibraries() { | ||
299 | return new String[] { | ||
300 | "SDL3", | ||
301 | // "SDL3_image", | ||
302 | // "SDL3_mixer", | ||
303 | // "SDL3_net", | ||
304 | // "SDL3_ttf", | ||
305 | "main" | ||
306 | }; | ||
307 | } | ||
308 | |||
309 | // Load the .so | ||
310 | public void loadLibraries() { | ||
311 | for (String lib : getLibraries()) { | ||
312 | SDL.loadLibrary(lib, this); | ||
313 | } | ||
314 | } | ||
315 | |||
316 | /** | ||
317 | * This method is called by SDL before starting the native application thread. | ||
318 | * It can be overridden to provide the arguments after the application name. | ||
319 | * The default implementation returns an empty array. It never returns null. | ||
320 | * @return arguments for the native application. | ||
321 | */ | ||
322 | protected String[] getArguments() { | ||
323 | return new String[0]; | ||
324 | } | ||
325 | |||
326 | public static void initialize() { | ||
327 | // The static nature of the singleton and Android quirkyness force us to initialize everything here | ||
328 | // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values | ||
329 | mSingleton = null; | ||
330 | mSurface = null; | ||
331 | mTextEdit = null; | ||
332 | mLayout = null; | ||
333 | mClipboardHandler = null; | ||
334 | mCursors = new Hashtable<Integer, PointerIcon>(); | ||
335 | mLastCursorID = 0; | ||
336 | mSDLThread = null; | ||
337 | mIsResumedCalled = false; | ||
338 | mHasFocus = true; | ||
339 | mNextNativeState = NativeState.INIT; | ||
340 | mCurrentNativeState = NativeState.INIT; | ||
341 | } | ||
342 | |||
343 | protected SDLSurface createSDLSurface(Context context) { | ||
344 | return new SDLSurface(context); | ||
345 | } | ||
346 | |||
347 | // Setup | ||
348 | @Override | ||
349 | protected void onCreate(Bundle savedInstanceState) { | ||
350 | Log.v(TAG, "Manufacturer: " + Build.MANUFACTURER); | ||
351 | Log.v(TAG, "Device: " + Build.DEVICE); | ||
352 | Log.v(TAG, "Model: " + Build.MODEL); | ||
353 | Log.v(TAG, "onCreate()"); | ||
354 | super.onCreate(savedInstanceState); | ||
355 | |||
356 | |||
357 | /* Control activity re-creation */ | ||
358 | if (mSDLMainFinished || mActivityCreated) { | ||
359 | boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); | ||
360 | if (mSDLMainFinished) { | ||
361 | Log.v(TAG, "SDL main() finished"); | ||
362 | } | ||
363 | if (allow_recreate) { | ||
364 | Log.v(TAG, "activity re-created"); | ||
365 | } else { | ||
366 | Log.v(TAG, "activity finished"); | ||
367 | System.exit(0); | ||
368 | return; | ||
369 | } | ||
370 | } | ||
371 | |||
372 | mActivityCreated = true; | ||
373 | |||
374 | try { | ||
375 | Thread.currentThread().setName("SDLActivity"); | ||
376 | } catch (Exception e) { | ||
377 | Log.v(TAG, "modify thread properties failed " + e.toString()); | ||
378 | } | ||
379 | |||
380 | // Load shared libraries | ||
381 | String errorMsgBrokenLib = ""; | ||
382 | try { | ||
383 | loadLibraries(); | ||
384 | mBrokenLibraries = false; /* success */ | ||
385 | } catch(UnsatisfiedLinkError e) { | ||
386 | System.err.println(e.getMessage()); | ||
387 | mBrokenLibraries = true; | ||
388 | errorMsgBrokenLib = e.getMessage(); | ||
389 | } catch(Exception e) { | ||
390 | System.err.println(e.getMessage()); | ||
391 | mBrokenLibraries = true; | ||
392 | errorMsgBrokenLib = e.getMessage(); | ||
393 | } | ||
394 | |||
395 | if (!mBrokenLibraries) { | ||
396 | String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." + | ||
397 | String.valueOf(SDL_MINOR_VERSION) + "." + | ||
398 | String.valueOf(SDL_MICRO_VERSION); | ||
399 | String version = nativeGetVersion(); | ||
400 | if (!version.equals(expected_version)) { | ||
401 | mBrokenLibraries = true; | ||
402 | errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")"; | ||
403 | } | ||
404 | } | ||
405 | |||
406 | if (mBrokenLibraries) { | ||
407 | mSingleton = this; | ||
408 | AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); | ||
409 | dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." | ||
410 | + System.getProperty("line.separator") | ||
411 | + System.getProperty("line.separator") | ||
412 | + "Error: " + errorMsgBrokenLib); | ||
413 | dlgAlert.setTitle("SDL Error"); | ||
414 | dlgAlert.setPositiveButton("Exit", | ||
415 | new DialogInterface.OnClickListener() { | ||
416 | @Override | ||
417 | public void onClick(DialogInterface dialog,int id) { | ||
418 | // if this button is clicked, close current activity | ||
419 | SDLActivity.mSingleton.finish(); | ||
420 | } | ||
421 | }); | ||
422 | dlgAlert.setCancelable(false); | ||
423 | dlgAlert.create().show(); | ||
424 | |||
425 | return; | ||
426 | } | ||
427 | |||
428 | |||
429 | /* Control activity re-creation */ | ||
430 | /* Robustness: check that the native code is run for the first time. | ||
431 | * (Maybe Activity was reset, but not the native code.) */ | ||
432 | { | ||
433 | int run_count = SDLActivity.nativeCheckSDLThreadCounter(); /* get and increment a native counter */ | ||
434 | if (run_count != 0) { | ||
435 | boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); | ||
436 | if (allow_recreate) { | ||
437 | Log.v(TAG, "activity re-created // run_count: " + run_count); | ||
438 | } else { | ||
439 | Log.v(TAG, "activity finished // run_count: " + run_count); | ||
440 | System.exit(0); | ||
441 | return; | ||
442 | } | ||
443 | } | ||
444 | } | ||
445 | |||
446 | // Set up JNI | ||
447 | SDL.setupJNI(); | ||
448 | |||
449 | // Initialize state | ||
450 | SDL.initialize(); | ||
451 | |||
452 | // So we can call stuff from static callbacks | ||
453 | mSingleton = this; | ||
454 | SDL.setContext(this); | ||
455 | |||
456 | mClipboardHandler = new SDLClipboardHandler(); | ||
457 | |||
458 | mHIDDeviceManager = HIDDeviceManager.acquire(this); | ||
459 | |||
460 | // Set up the surface | ||
461 | mSurface = createSDLSurface(this); | ||
462 | |||
463 | mLayout = new RelativeLayout(this); | ||
464 | mLayout.addView(mSurface); | ||
465 | |||
466 | // Get our current screen orientation and pass it down. | ||
467 | SDLActivity.nativeSetNaturalOrientation(SDLActivity.getNaturalOrientation()); | ||
468 | mCurrentRotation = SDLActivity.getCurrentRotation(); | ||
469 | SDLActivity.onNativeRotationChanged(mCurrentRotation); | ||
470 | |||
471 | try { | ||
472 | if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { | ||
473 | mCurrentLocale = getContext().getResources().getConfiguration().locale; | ||
474 | } else { | ||
475 | mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); | ||
476 | } | ||
477 | } catch(Exception ignored) { | ||
478 | } | ||
479 | |||
480 | switch (getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) { | ||
481 | case Configuration.UI_MODE_NIGHT_NO: | ||
482 | SDLActivity.onNativeDarkModeChanged(false); | ||
483 | break; | ||
484 | case Configuration.UI_MODE_NIGHT_YES: | ||
485 | SDLActivity.onNativeDarkModeChanged(true); | ||
486 | break; | ||
487 | } | ||
488 | |||
489 | setContentView(mLayout); | ||
490 | |||
491 | setWindowStyle(false); | ||
492 | |||
493 | getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); | ||
494 | |||
495 | // Get filename from "Open with" of another application | ||
496 | Intent intent = getIntent(); | ||
497 | if (intent != null && intent.getData() != null) { | ||
498 | String filename = intent.getData().getPath(); | ||
499 | if (filename != null) { | ||
500 | Log.v(TAG, "Got filename: " + filename); | ||
501 | SDLActivity.onNativeDropFile(filename); | ||
502 | } | ||
503 | } | ||
504 | } | ||
505 | |||
506 | protected void pauseNativeThread() { | ||
507 | mNextNativeState = NativeState.PAUSED; | ||
508 | mIsResumedCalled = false; | ||
509 | |||
510 | if (SDLActivity.mBrokenLibraries) { | ||
511 | return; | ||
512 | } | ||
513 | |||
514 | SDLActivity.handleNativeState(); | ||
515 | } | ||
516 | |||
517 | protected void resumeNativeThread() { | ||
518 | mNextNativeState = NativeState.RESUMED; | ||
519 | mIsResumedCalled = true; | ||
520 | |||
521 | if (SDLActivity.mBrokenLibraries) { | ||
522 | return; | ||
523 | } | ||
524 | |||
525 | SDLActivity.handleNativeState(); | ||
526 | } | ||
527 | |||
528 | // Events | ||
529 | @Override | ||
530 | protected void onPause() { | ||
531 | Log.v(TAG, "onPause()"); | ||
532 | super.onPause(); | ||
533 | |||
534 | if (mHIDDeviceManager != null) { | ||
535 | mHIDDeviceManager.setFrozen(true); | ||
536 | } | ||
537 | if (!mHasMultiWindow) { | ||
538 | pauseNativeThread(); | ||
539 | } | ||
540 | } | ||
541 | |||
542 | @Override | ||
543 | protected void onResume() { | ||
544 | Log.v(TAG, "onResume()"); | ||
545 | super.onResume(); | ||
546 | |||
547 | if (mHIDDeviceManager != null) { | ||
548 | mHIDDeviceManager.setFrozen(false); | ||
549 | } | ||
550 | if (!mHasMultiWindow) { | ||
551 | resumeNativeThread(); | ||
552 | } | ||
553 | } | ||
554 | |||
555 | @Override | ||
556 | protected void onStop() { | ||
557 | Log.v(TAG, "onStop()"); | ||
558 | super.onStop(); | ||
559 | if (mHasMultiWindow) { | ||
560 | pauseNativeThread(); | ||
561 | } | ||
562 | } | ||
563 | |||
564 | @Override | ||
565 | protected void onStart() { | ||
566 | Log.v(TAG, "onStart()"); | ||
567 | super.onStart(); | ||
568 | if (mHasMultiWindow) { | ||
569 | resumeNativeThread(); | ||
570 | } | ||
571 | } | ||
572 | |||
573 | public static int getNaturalOrientation() { | ||
574 | int result = SDL_ORIENTATION_UNKNOWN; | ||
575 | |||
576 | Activity activity = (Activity)getContext(); | ||
577 | if (activity != null) { | ||
578 | Configuration config = activity.getResources().getConfiguration(); | ||
579 | Display display = activity.getWindowManager().getDefaultDisplay(); | ||
580 | int rotation = display.getRotation(); | ||
581 | if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) && | ||
582 | config.orientation == Configuration.ORIENTATION_LANDSCAPE) || | ||
583 | ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) && | ||
584 | config.orientation == Configuration.ORIENTATION_PORTRAIT)) { | ||
585 | result = SDL_ORIENTATION_LANDSCAPE; | ||
586 | } else { | ||
587 | result = SDL_ORIENTATION_PORTRAIT; | ||
588 | } | ||
589 | } | ||
590 | return result; | ||
591 | } | ||
592 | |||
593 | public static int getCurrentRotation() { | ||
594 | int result = 0; | ||
595 | |||
596 | Activity activity = (Activity)getContext(); | ||
597 | if (activity != null) { | ||
598 | Display display = activity.getWindowManager().getDefaultDisplay(); | ||
599 | switch (display.getRotation()) { | ||
600 | case Surface.ROTATION_0: | ||
601 | result = 0; | ||
602 | break; | ||
603 | case Surface.ROTATION_90: | ||
604 | result = 90; | ||
605 | break; | ||
606 | case Surface.ROTATION_180: | ||
607 | result = 180; | ||
608 | break; | ||
609 | case Surface.ROTATION_270: | ||
610 | result = 270; | ||
611 | break; | ||
612 | } | ||
613 | } | ||
614 | return result; | ||
615 | } | ||
616 | |||
617 | @Override | ||
618 | public void onWindowFocusChanged(boolean hasFocus) { | ||
619 | super.onWindowFocusChanged(hasFocus); | ||
620 | Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); | ||
621 | |||
622 | if (SDLActivity.mBrokenLibraries) { | ||
623 | return; | ||
624 | } | ||
625 | |||
626 | mHasFocus = hasFocus; | ||
627 | if (hasFocus) { | ||
628 | mNextNativeState = NativeState.RESUMED; | ||
629 | SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded(); | ||
630 | |||
631 | SDLActivity.handleNativeState(); | ||
632 | nativeFocusChanged(true); | ||
633 | |||
634 | } else { | ||
635 | nativeFocusChanged(false); | ||
636 | if (!mHasMultiWindow) { | ||
637 | mNextNativeState = NativeState.PAUSED; | ||
638 | SDLActivity.handleNativeState(); | ||
639 | } | ||
640 | } | ||
641 | } | ||
642 | |||
643 | @Override | ||
644 | public void onTrimMemory(int level) { | ||
645 | Log.v(TAG, "onTrimMemory()"); | ||
646 | super.onTrimMemory(level); | ||
647 | |||
648 | if (SDLActivity.mBrokenLibraries) { | ||
649 | return; | ||
650 | } | ||
651 | |||
652 | SDLActivity.nativeLowMemory(); | ||
653 | } | ||
654 | |||
655 | @Override | ||
656 | public void onConfigurationChanged(Configuration newConfig) { | ||
657 | Log.v(TAG, "onConfigurationChanged()"); | ||
658 | super.onConfigurationChanged(newConfig); | ||
659 | |||
660 | if (SDLActivity.mBrokenLibraries) { | ||
661 | return; | ||
662 | } | ||
663 | |||
664 | if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) { | ||
665 | mCurrentLocale = newConfig.locale; | ||
666 | SDLActivity.onNativeLocaleChanged(); | ||
667 | } | ||
668 | |||
669 | switch (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) { | ||
670 | case Configuration.UI_MODE_NIGHT_NO: | ||
671 | SDLActivity.onNativeDarkModeChanged(false); | ||
672 | break; | ||
673 | case Configuration.UI_MODE_NIGHT_YES: | ||
674 | SDLActivity.onNativeDarkModeChanged(true); | ||
675 | break; | ||
676 | } | ||
677 | } | ||
678 | |||
679 | @Override | ||
680 | protected void onDestroy() { | ||
681 | Log.v(TAG, "onDestroy()"); | ||
682 | |||
683 | if (mHIDDeviceManager != null) { | ||
684 | HIDDeviceManager.release(mHIDDeviceManager); | ||
685 | mHIDDeviceManager = null; | ||
686 | } | ||
687 | |||
688 | SDLAudioManager.release(this); | ||
689 | |||
690 | if (SDLActivity.mBrokenLibraries) { | ||
691 | super.onDestroy(); | ||
692 | return; | ||
693 | } | ||
694 | |||
695 | if (SDLActivity.mSDLThread != null) { | ||
696 | |||
697 | // Send Quit event to "SDLThread" thread | ||
698 | SDLActivity.nativeSendQuit(); | ||
699 | |||
700 | // Wait for "SDLThread" thread to end | ||
701 | try { | ||
702 | // Use a timeout because: | ||
703 | // C SDLmain() thread might have started (mSDLThread.start() called) | ||
704 | // while the SDL_Init() might not have been called yet, | ||
705 | // and so the previous QUIT event will be discarded by SDL_Init() and app is running, not exiting. | ||
706 | SDLActivity.mSDLThread.join(1000); | ||
707 | } catch(Exception e) { | ||
708 | Log.v(TAG, "Problem stopping SDLThread: " + e); | ||
709 | } | ||
710 | } | ||
711 | |||
712 | SDLActivity.nativeQuit(); | ||
713 | |||
714 | super.onDestroy(); | ||
715 | } | ||
716 | |||
717 | @Override | ||
718 | public void onBackPressed() { | ||
719 | // Check if we want to block the back button in case of mouse right click. | ||
720 | // | ||
721 | // If we do, the normal hardware back button will no longer work and people have to use home, | ||
722 | // but the mouse right click will work. | ||
723 | // | ||
724 | boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false); | ||
725 | if (trapBack) { | ||
726 | // Exit and let the mouse handler handle this button (if appropriate) | ||
727 | return; | ||
728 | } | ||
729 | |||
730 | // Default system back button behavior. | ||
731 | if (!isFinishing()) { | ||
732 | super.onBackPressed(); | ||
733 | } | ||
734 | } | ||
735 | |||
736 | @Override | ||
737 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||
738 | super.onActivityResult(requestCode, resultCode, data); | ||
739 | |||
740 | if (mFileDialogState != null && mFileDialogState.requestCode == requestCode) { | ||
741 | /* This is our file dialog */ | ||
742 | String[] filelist = null; | ||
743 | |||
744 | if (data != null) { | ||
745 | Uri singleFileUri = data.getData(); | ||
746 | |||
747 | if (singleFileUri == null) { | ||
748 | /* Use Intent.getClipData to get multiple choices */ | ||
749 | ClipData clipData = data.getClipData(); | ||
750 | assert clipData != null; | ||
751 | |||
752 | filelist = new String[clipData.getItemCount()]; | ||
753 | |||
754 | for (int i = 0; i < filelist.length; i++) { | ||
755 | String uri = clipData.getItemAt(i).getUri().toString(); | ||
756 | filelist[i] = uri; | ||
757 | } | ||
758 | } else { | ||
759 | /* Only one file is selected. */ | ||
760 | filelist = new String[]{singleFileUri.toString()}; | ||
761 | } | ||
762 | } else { | ||
763 | /* User cancelled the request. */ | ||
764 | filelist = new String[0]; | ||
765 | } | ||
766 | |||
767 | // TODO: Detect the file MIME type and pass the filter value accordingly. | ||
768 | SDLActivity.onNativeFileDialog(requestCode, filelist, -1); | ||
769 | mFileDialogState = null; | ||
770 | } | ||
771 | } | ||
772 | |||
773 | // Called by JNI from SDL. | ||
774 | public static void manualBackButton() { | ||
775 | mSingleton.pressBackButton(); | ||
776 | } | ||
777 | |||
778 | // Used to get us onto the activity's main thread | ||
779 | public void pressBackButton() { | ||
780 | runOnUiThread(new Runnable() { | ||
781 | @Override | ||
782 | public void run() { | ||
783 | if (!SDLActivity.this.isFinishing()) { | ||
784 | SDLActivity.this.superOnBackPressed(); | ||
785 | } | ||
786 | } | ||
787 | }); | ||
788 | } | ||
789 | |||
790 | // Used to access the system back behavior. | ||
791 | public void superOnBackPressed() { | ||
792 | super.onBackPressed(); | ||
793 | } | ||
794 | |||
795 | @Override | ||
796 | public boolean dispatchKeyEvent(KeyEvent event) { | ||
797 | |||
798 | if (SDLActivity.mBrokenLibraries) { | ||
799 | return false; | ||
800 | } | ||
801 | |||
802 | int keyCode = event.getKeyCode(); | ||
803 | // Ignore certain special keys so they're handled by Android | ||
804 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || | ||
805 | keyCode == KeyEvent.KEYCODE_VOLUME_UP || | ||
806 | keyCode == KeyEvent.KEYCODE_CAMERA || | ||
807 | keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ | ||
808 | keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ | ||
809 | ) { | ||
810 | return false; | ||
811 | } | ||
812 | mDispatchingKeyEvent = true; | ||
813 | boolean result = super.dispatchKeyEvent(event); | ||
814 | mDispatchingKeyEvent = false; | ||
815 | return result; | ||
816 | } | ||
817 | |||
818 | public static boolean dispatchingKeyEvent() { | ||
819 | return mDispatchingKeyEvent; | ||
820 | } | ||
821 | |||
822 | /* Transition to next state */ | ||
823 | public static void handleNativeState() { | ||
824 | |||
825 | if (mNextNativeState == mCurrentNativeState) { | ||
826 | // Already in same state, discard. | ||
827 | return; | ||
828 | } | ||
829 | |||
830 | // Try a transition to init state | ||
831 | if (mNextNativeState == NativeState.INIT) { | ||
832 | |||
833 | mCurrentNativeState = mNextNativeState; | ||
834 | return; | ||
835 | } | ||
836 | |||
837 | // Try a transition to paused state | ||
838 | if (mNextNativeState == NativeState.PAUSED) { | ||
839 | if (mSDLThread != null) { | ||
840 | nativePause(); | ||
841 | } | ||
842 | if (mSurface != null) { | ||
843 | mSurface.handlePause(); | ||
844 | } | ||
845 | mCurrentNativeState = mNextNativeState; | ||
846 | return; | ||
847 | } | ||
848 | |||
849 | // Try a transition to resumed state | ||
850 | if (mNextNativeState == NativeState.RESUMED) { | ||
851 | if (mSurface.mIsSurfaceReady && (mHasFocus || mHasMultiWindow) && mIsResumedCalled) { | ||
852 | if (mSDLThread == null) { | ||
853 | // This is the entry point to the C app. | ||
854 | // Start up the C app thread and enable sensor input for the first time | ||
855 | // FIXME: Why aren't we enabling sensor input at start? | ||
856 | |||
857 | mSDLThread = new Thread(new SDLMain(), "SDLThread"); | ||
858 | mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); | ||
859 | mSDLThread.start(); | ||
860 | |||
861 | // No nativeResume(), don't signal Android_ResumeSem | ||
862 | } else { | ||
863 | nativeResume(); | ||
864 | } | ||
865 | mSurface.handleResume(); | ||
866 | |||
867 | mCurrentNativeState = mNextNativeState; | ||
868 | } | ||
869 | } | ||
870 | } | ||
871 | |||
872 | // Messages from the SDLMain thread | ||
873 | protected static final int COMMAND_CHANGE_TITLE = 1; | ||
874 | protected static final int COMMAND_CHANGE_WINDOW_STYLE = 2; | ||
875 | protected static final int COMMAND_TEXTEDIT_HIDE = 3; | ||
876 | protected static final int COMMAND_SET_KEEP_SCREEN_ON = 5; | ||
877 | protected static final int COMMAND_USER = 0x8000; | ||
878 | |||
879 | protected static boolean mFullscreenModeActive; | ||
880 | |||
881 | /** | ||
882 | * This method is called by SDL if SDL did not handle a message itself. | ||
883 | * This happens if a received message contains an unsupported command. | ||
884 | * Method can be overwritten to handle Messages in a different class. | ||
885 | * @param command the command of the message. | ||
886 | * @param param the parameter of the message. May be null. | ||
887 | * @return if the message was handled in overridden method. | ||
888 | */ | ||
889 | protected boolean onUnhandledMessage(int command, Object param) { | ||
890 | return false; | ||
891 | } | ||
892 | |||
893 | /** | ||
894 | * A Handler class for Messages from native SDL applications. | ||
895 | * It uses current Activities as target (e.g. for the title). | ||
896 | * static to prevent implicit references to enclosing object. | ||
897 | */ | ||
898 | protected static class SDLCommandHandler extends Handler { | ||
899 | @Override | ||
900 | public void handleMessage(Message msg) { | ||
901 | Context context = SDL.getContext(); | ||
902 | if (context == null) { | ||
903 | Log.e(TAG, "error handling message, getContext() returned null"); | ||
904 | return; | ||
905 | } | ||
906 | switch (msg.arg1) { | ||
907 | case COMMAND_CHANGE_TITLE: | ||
908 | if (context instanceof Activity) { | ||
909 | ((Activity) context).setTitle((String)msg.obj); | ||
910 | } else { | ||
911 | Log.e(TAG, "error handling message, getContext() returned no Activity"); | ||
912 | } | ||
913 | break; | ||
914 | case COMMAND_CHANGE_WINDOW_STYLE: | ||
915 | if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { | ||
916 | if (context instanceof Activity) { | ||
917 | Window window = ((Activity) context).getWindow(); | ||
918 | if (window != null) { | ||
919 | if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { | ||
920 | int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | | ||
921 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | | ||
922 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | | ||
923 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | | ||
924 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | | ||
925 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; | ||
926 | window.getDecorView().setSystemUiVisibility(flags); | ||
927 | window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); | ||
928 | window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); | ||
929 | SDLActivity.mFullscreenModeActive = true; | ||
930 | } else { | ||
931 | int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; | ||
932 | window.getDecorView().setSystemUiVisibility(flags); | ||
933 | window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); | ||
934 | window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); | ||
935 | SDLActivity.mFullscreenModeActive = false; | ||
936 | } | ||
937 | if (Build.VERSION.SDK_INT >= 28 /* Android 9 (Pie) */) { | ||
938 | window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; | ||
939 | } | ||
940 | if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */ && | ||
941 | Build.VERSION.SDK_INT < 35 /* Android 15 */) { | ||
942 | SDLActivity.onNativeInsetsChanged(0, 0, 0, 0); | ||
943 | } | ||
944 | } | ||
945 | } else { | ||
946 | Log.e(TAG, "error handling message, getContext() returned no Activity"); | ||
947 | } | ||
948 | } | ||
949 | break; | ||
950 | case COMMAND_TEXTEDIT_HIDE: | ||
951 | if (mTextEdit != null) { | ||
952 | // Note: On some devices setting view to GONE creates a flicker in landscape. | ||
953 | // Setting the View's sizes to 0 is similar to GONE but without the flicker. | ||
954 | // The sizes will be set to useful values when the keyboard is shown again. | ||
955 | mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); | ||
956 | |||
957 | InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); | ||
958 | imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); | ||
959 | |||
960 | mScreenKeyboardShown = false; | ||
961 | |||
962 | mSurface.requestFocus(); | ||
963 | } | ||
964 | break; | ||
965 | case COMMAND_SET_KEEP_SCREEN_ON: | ||
966 | { | ||
967 | if (context instanceof Activity) { | ||
968 | Window window = ((Activity) context).getWindow(); | ||
969 | if (window != null) { | ||
970 | if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { | ||
971 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); | ||
972 | } else { | ||
973 | window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); | ||
974 | } | ||
975 | } | ||
976 | } | ||
977 | break; | ||
978 | } | ||
979 | default: | ||
980 | if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { | ||
981 | Log.e(TAG, "error handling message, command is " + msg.arg1); | ||
982 | } | ||
983 | } | ||
984 | } | ||
985 | } | ||
986 | |||
987 | // Handler for the messages | ||
988 | Handler commandHandler = new SDLCommandHandler(); | ||
989 | |||
990 | // Send a message from the SDLMain thread | ||
991 | protected boolean sendCommand(int command, Object data) { | ||
992 | Message msg = commandHandler.obtainMessage(); | ||
993 | msg.arg1 = command; | ||
994 | msg.obj = data; | ||
995 | boolean result = commandHandler.sendMessage(msg); | ||
996 | |||
997 | if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { | ||
998 | if (command == COMMAND_CHANGE_WINDOW_STYLE) { | ||
999 | // Ensure we don't return until the resize has actually happened, | ||
1000 | // or 500ms have passed. | ||
1001 | |||
1002 | boolean bShouldWait = false; | ||
1003 | |||
1004 | if (data instanceof Integer) { | ||
1005 | // Let's figure out if we're already laid out fullscreen or not. | ||
1006 | Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); | ||
1007 | DisplayMetrics realMetrics = new DisplayMetrics(); | ||
1008 | display.getRealMetrics(realMetrics); | ||
1009 | |||
1010 | boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && | ||
1011 | (realMetrics.heightPixels == mSurface.getHeight())); | ||
1012 | |||
1013 | if ((Integer) data == 1) { | ||
1014 | // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going | ||
1015 | // to change size and should wait for surfaceChanged() before we return, so the size | ||
1016 | // is right back in native code. If we're already laid out fullscreen, though, we're | ||
1017 | // not going to change size even if we change decor modes, so we shouldn't wait for | ||
1018 | // surfaceChanged() -- which may not even happen -- and should return immediately. | ||
1019 | bShouldWait = !bFullscreenLayout; | ||
1020 | } else { | ||
1021 | // If we're laid out fullscreen (even if the status bar and nav bar are present), | ||
1022 | // or are actively in fullscreen, we're going to change size and should wait for | ||
1023 | // surfaceChanged before we return, so the size is right back in native code. | ||
1024 | bShouldWait = bFullscreenLayout; | ||
1025 | } | ||
1026 | } | ||
1027 | |||
1028 | if (bShouldWait && (SDLActivity.getContext() != null)) { | ||
1029 | // We'll wait for the surfaceChanged() method, which will notify us | ||
1030 | // when called. That way, we know our current size is really the | ||
1031 | // size we need, instead of grabbing a size that's still got | ||
1032 | // the navigation and/or status bars before they're hidden. | ||
1033 | // | ||
1034 | // We'll wait for up to half a second, because some devices | ||
1035 | // take a surprisingly long time for the surface resize, but | ||
1036 | // then we'll just give up and return. | ||
1037 | // | ||
1038 | synchronized (SDLActivity.getContext()) { | ||
1039 | try { | ||
1040 | SDLActivity.getContext().wait(500); | ||
1041 | } catch (InterruptedException ie) { | ||
1042 | ie.printStackTrace(); | ||
1043 | } | ||
1044 | } | ||
1045 | } | ||
1046 | } | ||
1047 | } | ||
1048 | |||
1049 | return result; | ||
1050 | } | ||
1051 | |||
1052 | // C functions we call | ||
1053 | public static native String nativeGetVersion(); | ||
1054 | public static native int nativeSetupJNI(); | ||
1055 | public static native void nativeInitMainThread(); | ||
1056 | public static native void nativeCleanupMainThread(); | ||
1057 | public static native int nativeRunMain(String library, String function, Object arguments); | ||
1058 | public static native void nativeLowMemory(); | ||
1059 | public static native void nativeSendQuit(); | ||
1060 | public static native void nativeQuit(); | ||
1061 | public static native void nativePause(); | ||
1062 | public static native void nativeResume(); | ||
1063 | public static native void nativeFocusChanged(boolean hasFocus); | ||
1064 | public static native void onNativeDropFile(String filename); | ||
1065 | public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float density, float rate); | ||
1066 | public static native void onNativeResize(); | ||
1067 | public static native void onNativeKeyDown(int keycode); | ||
1068 | public static native void onNativeKeyUp(int keycode); | ||
1069 | public static native boolean onNativeSoftReturnKey(); | ||
1070 | public static native void onNativeKeyboardFocusLost(); | ||
1071 | public static native void onNativeMouse(int button, int action, float x, float y, boolean relative); | ||
1072 | public static native void onNativeTouch(int touchDevId, int pointerFingerId, | ||
1073 | int action, float x, | ||
1074 | float y, float p); | ||
1075 | public static native void onNativePen(int penId, int button, int action, float x, float y, float p); | ||
1076 | public static native void onNativeAccel(float x, float y, float z); | ||
1077 | public static native void onNativeClipboardChanged(); | ||
1078 | public static native void onNativeSurfaceCreated(); | ||
1079 | public static native void onNativeSurfaceChanged(); | ||
1080 | public static native void onNativeSurfaceDestroyed(); | ||
1081 | public static native String nativeGetHint(String name); | ||
1082 | public static native boolean nativeGetHintBoolean(String name, boolean default_value); | ||
1083 | public static native void nativeSetenv(String name, String value); | ||
1084 | public static native void nativeSetNaturalOrientation(int orientation); | ||
1085 | public static native void onNativeRotationChanged(int rotation); | ||
1086 | public static native void onNativeInsetsChanged(int left, int right, int top, int bottom); | ||
1087 | public static native void nativeAddTouch(int touchId, String name); | ||
1088 | public static native void nativePermissionResult(int requestCode, boolean result); | ||
1089 | public static native void onNativeLocaleChanged(); | ||
1090 | public static native void onNativeDarkModeChanged(boolean enabled); | ||
1091 | public static native boolean nativeAllowRecreateActivity(); | ||
1092 | public static native int nativeCheckSDLThreadCounter(); | ||
1093 | public static native void onNativeFileDialog(int requestCode, String[] filelist, int filter); | ||
1094 | |||
1095 | /** | ||
1096 | * This method is called by SDL using JNI. | ||
1097 | */ | ||
1098 | public static boolean setActivityTitle(String title) { | ||
1099 | // Called from SDLMain() thread and can't directly affect the view | ||
1100 | return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); | ||
1101 | } | ||
1102 | |||
1103 | /** | ||
1104 | * This method is called by SDL using JNI. | ||
1105 | */ | ||
1106 | public static void setWindowStyle(boolean fullscreen) { | ||
1107 | // Called from SDLMain() thread and can't directly affect the view | ||
1108 | mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0); | ||
1109 | } | ||
1110 | |||
1111 | /** | ||
1112 | * This method is called by SDL using JNI. | ||
1113 | * This is a static method for JNI convenience, it calls a non-static method | ||
1114 | * so that is can be overridden | ||
1115 | */ | ||
1116 | public static void setOrientation(int w, int h, boolean resizable, String hint) | ||
1117 | { | ||
1118 | if (mSingleton != null) { | ||
1119 | mSingleton.setOrientationBis(w, h, resizable, hint); | ||
1120 | } | ||
1121 | } | ||
1122 | |||
1123 | /** | ||
1124 | * This can be overridden | ||
1125 | */ | ||
1126 | public void setOrientationBis(int w, int h, boolean resizable, String hint) | ||
1127 | { | ||
1128 | int orientation_landscape = -1; | ||
1129 | int orientation_portrait = -1; | ||
1130 | |||
1131 | /* If set, hint "explicitly controls which UI orientations are allowed". */ | ||
1132 | if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { | ||
1133 | orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; | ||
1134 | } else if (hint.contains("LandscapeLeft")) { | ||
1135 | orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; | ||
1136 | } else if (hint.contains("LandscapeRight")) { | ||
1137 | orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; | ||
1138 | } | ||
1139 | |||
1140 | /* exact match to 'Portrait' to distinguish with PortraitUpsideDown */ | ||
1141 | boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); | ||
1142 | |||
1143 | if (contains_Portrait && hint.contains("PortraitUpsideDown")) { | ||
1144 | orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; | ||
1145 | } else if (contains_Portrait) { | ||
1146 | orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; | ||
1147 | } else if (hint.contains("PortraitUpsideDown")) { | ||
1148 | orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; | ||
1149 | } | ||
1150 | |||
1151 | boolean is_landscape_allowed = (orientation_landscape != -1); | ||
1152 | boolean is_portrait_allowed = (orientation_portrait != -1); | ||
1153 | int req; /* Requested orientation */ | ||
1154 | |||
1155 | /* No valid hint, nothing is explicitly allowed */ | ||
1156 | if (!is_portrait_allowed && !is_landscape_allowed) { | ||
1157 | if (resizable) { | ||
1158 | /* All orientations are allowed, respecting user orientation lock setting */ | ||
1159 | req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; | ||
1160 | } else { | ||
1161 | /* Fixed window and nothing specified. Get orientation from w/h of created window */ | ||
1162 | req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); | ||
1163 | } | ||
1164 | } else { | ||
1165 | /* At least one orientation is allowed */ | ||
1166 | if (resizable) { | ||
1167 | if (is_portrait_allowed && is_landscape_allowed) { | ||
1168 | /* hint allows both landscape and portrait, promote to full user */ | ||
1169 | req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; | ||
1170 | } else { | ||
1171 | /* Use the only one allowed "orientation" */ | ||
1172 | req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); | ||
1173 | } | ||
1174 | } else { | ||
1175 | /* Fixed window and both orientations are allowed. Choose one. */ | ||
1176 | if (is_portrait_allowed && is_landscape_allowed) { | ||
1177 | req = (w > h ? orientation_landscape : orientation_portrait); | ||
1178 | } else { | ||
1179 | /* Use the only one allowed "orientation" */ | ||
1180 | req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); | ||
1181 | } | ||
1182 | } | ||
1183 | } | ||
1184 | |||
1185 | Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); | ||
1186 | mSingleton.setRequestedOrientation(req); | ||
1187 | } | ||
1188 | |||
1189 | /** | ||
1190 | * This method is called by SDL using JNI. | ||
1191 | */ | ||
1192 | public static void minimizeWindow() { | ||
1193 | |||
1194 | if (mSingleton == null) { | ||
1195 | return; | ||
1196 | } | ||
1197 | |||
1198 | Intent startMain = new Intent(Intent.ACTION_MAIN); | ||
1199 | startMain.addCategory(Intent.CATEGORY_HOME); | ||
1200 | startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||
1201 | mSingleton.startActivity(startMain); | ||
1202 | } | ||
1203 | |||
1204 | /** | ||
1205 | * This method is called by SDL using JNI. | ||
1206 | */ | ||
1207 | public static boolean shouldMinimizeOnFocusLoss() { | ||
1208 | return false; | ||
1209 | } | ||
1210 | |||
1211 | /** | ||
1212 | * This method is called by SDL using JNI. | ||
1213 | */ | ||
1214 | public static boolean isScreenKeyboardShown() | ||
1215 | { | ||
1216 | if (mTextEdit == null) { | ||
1217 | return false; | ||
1218 | } | ||
1219 | |||
1220 | if (!mScreenKeyboardShown) { | ||
1221 | return false; | ||
1222 | } | ||
1223 | |||
1224 | InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | ||
1225 | return imm.isAcceptingText(); | ||
1226 | |||
1227 | } | ||
1228 | |||
1229 | /** | ||
1230 | * This method is called by SDL using JNI. | ||
1231 | */ | ||
1232 | public static boolean supportsRelativeMouse() | ||
1233 | { | ||
1234 | // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under | ||
1235 | // Android 7 APIs, and simply returns no data under Android 8 APIs. | ||
1236 | // | ||
1237 | // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and | ||
1238 | // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result, | ||
1239 | // we should stick to relative mode. | ||
1240 | // | ||
1241 | if (Build.VERSION.SDK_INT < 27 /* Android 8.1 (O_MR1) */ && isDeXMode()) { | ||
1242 | return false; | ||
1243 | } | ||
1244 | |||
1245 | return SDLActivity.getMotionListener().supportsRelativeMouse(); | ||
1246 | } | ||
1247 | |||
1248 | /** | ||
1249 | * This method is called by SDL using JNI. | ||
1250 | */ | ||
1251 | public static boolean setRelativeMouseEnabled(boolean enabled) | ||
1252 | { | ||
1253 | if (enabled && !supportsRelativeMouse()) { | ||
1254 | return false; | ||
1255 | } | ||
1256 | |||
1257 | return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled); | ||
1258 | } | ||
1259 | |||
1260 | /** | ||
1261 | * This method is called by SDL using JNI. | ||
1262 | */ | ||
1263 | public static boolean sendMessage(int command, int param) { | ||
1264 | if (mSingleton == null) { | ||
1265 | return false; | ||
1266 | } | ||
1267 | return mSingleton.sendCommand(command, param); | ||
1268 | } | ||
1269 | |||
1270 | /** | ||
1271 | * This method is called by SDL using JNI. | ||
1272 | */ | ||
1273 | public static Context getContext() { | ||
1274 | return SDL.getContext(); | ||
1275 | } | ||
1276 | |||
1277 | /** | ||
1278 | * This method is called by SDL using JNI. | ||
1279 | */ | ||
1280 | public static boolean isAndroidTV() { | ||
1281 | UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE); | ||
1282 | if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { | ||
1283 | return true; | ||
1284 | } | ||
1285 | if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { | ||
1286 | return true; | ||
1287 | } | ||
1288 | if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { | ||
1289 | return true; | ||
1290 | } | ||
1291 | if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV")) { | ||
1292 | return true; | ||
1293 | } | ||
1294 | return false; | ||
1295 | } | ||
1296 | |||
1297 | public static boolean isVRHeadset() { | ||
1298 | if (Build.MANUFACTURER.equals("Oculus") && Build.MODEL.startsWith("Quest")) { | ||
1299 | return true; | ||
1300 | } | ||
1301 | if (Build.MANUFACTURER.equals("Pico")) { | ||
1302 | return true; | ||
1303 | } | ||
1304 | return false; | ||
1305 | } | ||
1306 | |||
1307 | public static double getDiagonal() | ||
1308 | { | ||
1309 | DisplayMetrics metrics = new DisplayMetrics(); | ||
1310 | Activity activity = (Activity)getContext(); | ||
1311 | if (activity == null) { | ||
1312 | return 0.0; | ||
1313 | } | ||
1314 | activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); | ||
1315 | |||
1316 | double dWidthInches = metrics.widthPixels / (double)metrics.xdpi; | ||
1317 | double dHeightInches = metrics.heightPixels / (double)metrics.ydpi; | ||
1318 | |||
1319 | return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); | ||
1320 | } | ||
1321 | |||
1322 | /** | ||
1323 | * This method is called by SDL using JNI. | ||
1324 | */ | ||
1325 | public static boolean isTablet() { | ||
1326 | // If our diagonal size is seven inches or greater, we consider ourselves a tablet. | ||
1327 | return (getDiagonal() >= 7.0); | ||
1328 | } | ||
1329 | |||
1330 | /** | ||
1331 | * This method is called by SDL using JNI. | ||
1332 | */ | ||
1333 | public static boolean isChromebook() { | ||
1334 | if (getContext() == null) { | ||
1335 | return false; | ||
1336 | } | ||
1337 | return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); | ||
1338 | } | ||
1339 | |||
1340 | /** | ||
1341 | * This method is called by SDL using JNI. | ||
1342 | */ | ||
1343 | public static boolean isDeXMode() { | ||
1344 | if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { | ||
1345 | return false; | ||
1346 | } | ||
1347 | try { | ||
1348 | final Configuration config = getContext().getResources().getConfiguration(); | ||
1349 | final Class<?> configClass = config.getClass(); | ||
1350 | return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) | ||
1351 | == configClass.getField("semDesktopModeEnabled").getInt(config); | ||
1352 | } catch(Exception ignored) { | ||
1353 | return false; | ||
1354 | } | ||
1355 | } | ||
1356 | |||
1357 | /** | ||
1358 | * This method is called by SDL using JNI. | ||
1359 | */ | ||
1360 | public static boolean getManifestEnvironmentVariables() { | ||
1361 | try { | ||
1362 | if (getContext() == null) { | ||
1363 | return false; | ||
1364 | } | ||
1365 | |||
1366 | ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); | ||
1367 | Bundle bundle = applicationInfo.metaData; | ||
1368 | if (bundle == null) { | ||
1369 | return false; | ||
1370 | } | ||
1371 | String prefix = "SDL_ENV."; | ||
1372 | final int trimLength = prefix.length(); | ||
1373 | for (String key : bundle.keySet()) { | ||
1374 | if (key.startsWith(prefix)) { | ||
1375 | String name = key.substring(trimLength); | ||
1376 | String value = bundle.get(key).toString(); | ||
1377 | nativeSetenv(name, value); | ||
1378 | } | ||
1379 | } | ||
1380 | /* environment variables set! */ | ||
1381 | return true; | ||
1382 | } catch (Exception e) { | ||
1383 | Log.v(TAG, "exception " + e.toString()); | ||
1384 | } | ||
1385 | return false; | ||
1386 | } | ||
1387 | |||
1388 | // This method is called by SDLControllerManager's API 26 Generic Motion Handler. | ||
1389 | public static View getContentView() { | ||
1390 | return mLayout; | ||
1391 | } | ||
1392 | |||
1393 | static class ShowTextInputTask implements Runnable { | ||
1394 | /* | ||
1395 | * This is used to regulate the pan&scan method to have some offset from | ||
1396 | * the bottom edge of the input region and the top edge of an input | ||
1397 | * method (soft keyboard) | ||
1398 | */ | ||
1399 | static final int HEIGHT_PADDING = 15; | ||
1400 | |||
1401 | public int input_type; | ||
1402 | public int x, y, w, h; | ||
1403 | |||
1404 | public ShowTextInputTask(int input_type, int x, int y, int w, int h) { | ||
1405 | this.input_type = input_type; | ||
1406 | this.x = x; | ||
1407 | this.y = y; | ||
1408 | this.w = w; | ||
1409 | this.h = h; | ||
1410 | |||
1411 | /* Minimum size of 1 pixel, so it takes focus. */ | ||
1412 | if (this.w <= 0) { | ||
1413 | this.w = 1; | ||
1414 | } | ||
1415 | if (this.h + HEIGHT_PADDING <= 0) { | ||
1416 | this.h = 1 - HEIGHT_PADDING; | ||
1417 | } | ||
1418 | } | ||
1419 | |||
1420 | @Override | ||
1421 | public void run() { | ||
1422 | RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); | ||
1423 | params.leftMargin = x; | ||
1424 | params.topMargin = y; | ||
1425 | |||
1426 | if (mTextEdit == null) { | ||
1427 | mTextEdit = new SDLDummyEdit(SDL.getContext()); | ||
1428 | |||
1429 | mLayout.addView(mTextEdit, params); | ||
1430 | } else { | ||
1431 | mTextEdit.setLayoutParams(params); | ||
1432 | } | ||
1433 | mTextEdit.setInputType(input_type); | ||
1434 | |||
1435 | mTextEdit.setVisibility(View.VISIBLE); | ||
1436 | mTextEdit.requestFocus(); | ||
1437 | |||
1438 | InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | ||
1439 | imm.showSoftInput(mTextEdit, 0); | ||
1440 | |||
1441 | mScreenKeyboardShown = true; | ||
1442 | } | ||
1443 | } | ||
1444 | |||
1445 | /** | ||
1446 | * This method is called by SDL using JNI. | ||
1447 | */ | ||
1448 | public static boolean showTextInput(int input_type, int x, int y, int w, int h) { | ||
1449 | // Transfer the task to the main thread as a Runnable | ||
1450 | return mSingleton.commandHandler.post(new ShowTextInputTask(input_type, x, y, w, h)); | ||
1451 | } | ||
1452 | |||
1453 | public static boolean isTextInputEvent(KeyEvent event) { | ||
1454 | |||
1455 | // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT | ||
1456 | if (event.isCtrlPressed()) { | ||
1457 | return false; | ||
1458 | } | ||
1459 | |||
1460 | return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; | ||
1461 | } | ||
1462 | |||
1463 | public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) { | ||
1464 | int deviceId = event.getDeviceId(); | ||
1465 | int source = event.getSource(); | ||
1466 | |||
1467 | if (source == InputDevice.SOURCE_UNKNOWN) { | ||
1468 | InputDevice device = InputDevice.getDevice(deviceId); | ||
1469 | if (device != null) { | ||
1470 | source = device.getSources(); | ||
1471 | } | ||
1472 | } | ||
1473 | |||
1474 | // if (event.getAction() == KeyEvent.ACTION_DOWN) { | ||
1475 | // Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); | ||
1476 | // } else if (event.getAction() == KeyEvent.ACTION_UP) { | ||
1477 | // Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); | ||
1478 | // } | ||
1479 | |||
1480 | // Dispatch the different events depending on where they come from | ||
1481 | // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD | ||
1482 | // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD | ||
1483 | // | ||
1484 | // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and | ||
1485 | // SOURCE_JOYSTICK, while its key events arrive from the keyboard source | ||
1486 | // So, retrieve the device itself and check all of its sources | ||
1487 | if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { | ||
1488 | // Note that we process events with specific key codes here | ||
1489 | if (event.getAction() == KeyEvent.ACTION_DOWN) { | ||
1490 | if (SDLControllerManager.onNativePadDown(deviceId, keyCode)) { | ||
1491 | return true; | ||
1492 | } | ||
1493 | } else if (event.getAction() == KeyEvent.ACTION_UP) { | ||
1494 | if (SDLControllerManager.onNativePadUp(deviceId, keyCode)) { | ||
1495 | return true; | ||
1496 | } | ||
1497 | } | ||
1498 | } | ||
1499 | |||
1500 | if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { | ||
1501 | if (SDLActivity.isVRHeadset()) { | ||
1502 | // The Oculus Quest controller back button comes in as source mouse, so accept that | ||
1503 | } else { | ||
1504 | // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses | ||
1505 | // they are ignored here because sending them as mouse input to SDL is messy | ||
1506 | if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { | ||
1507 | switch (event.getAction()) { | ||
1508 | case KeyEvent.ACTION_DOWN: | ||
1509 | case KeyEvent.ACTION_UP: | ||
1510 | // mark the event as handled or it will be handled by system | ||
1511 | // handling KEYCODE_BACK by system will call onBackPressed() | ||
1512 | return true; | ||
1513 | } | ||
1514 | } | ||
1515 | } | ||
1516 | } | ||
1517 | |||
1518 | if (event.getAction() == KeyEvent.ACTION_DOWN) { | ||
1519 | onNativeKeyDown(keyCode); | ||
1520 | |||
1521 | if (isTextInputEvent(event)) { | ||
1522 | if (ic != null) { | ||
1523 | ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); | ||
1524 | } else { | ||
1525 | SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); | ||
1526 | } | ||
1527 | } | ||
1528 | return true; | ||
1529 | } else if (event.getAction() == KeyEvent.ACTION_UP) { | ||
1530 | onNativeKeyUp(keyCode); | ||
1531 | return true; | ||
1532 | } | ||
1533 | |||
1534 | return false; | ||
1535 | } | ||
1536 | |||
1537 | /** | ||
1538 | * This method is called by SDL using JNI. | ||
1539 | */ | ||
1540 | public static Surface getNativeSurface() { | ||
1541 | if (SDLActivity.mSurface == null) { | ||
1542 | return null; | ||
1543 | } | ||
1544 | return SDLActivity.mSurface.getNativeSurface(); | ||
1545 | } | ||
1546 | |||
1547 | // Input | ||
1548 | |||
1549 | /** | ||
1550 | * This method is called by SDL using JNI. | ||
1551 | */ | ||
1552 | public static void initTouch() { | ||
1553 | int[] ids = InputDevice.getDeviceIds(); | ||
1554 | |||
1555 | for (int id : ids) { | ||
1556 | InputDevice device = InputDevice.getDevice(id); | ||
1557 | /* Allow SOURCE_TOUCHSCREEN and also Virtual InputDevices because they can send TOUCHSCREEN events */ | ||
1558 | if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN | ||
1559 | || device.isVirtual())) { | ||
1560 | |||
1561 | nativeAddTouch(device.getId(), device.getName()); | ||
1562 | } | ||
1563 | } | ||
1564 | } | ||
1565 | |||
1566 | // Messagebox | ||
1567 | |||
1568 | /** Result of current messagebox. Also used for blocking the calling thread. */ | ||
1569 | protected final int[] messageboxSelection = new int[1]; | ||
1570 | |||
1571 | /** | ||
1572 | * This method is called by SDL using JNI. | ||
1573 | * Shows the messagebox from UI thread and block calling thread. | ||
1574 | * buttonFlags, buttonIds and buttonTexts must have same length. | ||
1575 | * @param buttonFlags array containing flags for every button. | ||
1576 | * @param buttonIds array containing id for every button. | ||
1577 | * @param buttonTexts array containing text for every button. | ||
1578 | * @param colors null for default or array of length 5 containing colors. | ||
1579 | * @return button id or -1. | ||
1580 | */ | ||
1581 | public int messageboxShowMessageBox( | ||
1582 | final int flags, | ||
1583 | final String title, | ||
1584 | final String message, | ||
1585 | final int[] buttonFlags, | ||
1586 | final int[] buttonIds, | ||
1587 | final String[] buttonTexts, | ||
1588 | final int[] colors) { | ||
1589 | |||
1590 | messageboxSelection[0] = -1; | ||
1591 | |||
1592 | // sanity checks | ||
1593 | |||
1594 | if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { | ||
1595 | return -1; // implementation broken | ||
1596 | } | ||
1597 | |||
1598 | // collect arguments for Dialog | ||
1599 | |||
1600 | final Bundle args = new Bundle(); | ||
1601 | args.putInt("flags", flags); | ||
1602 | args.putString("title", title); | ||
1603 | args.putString("message", message); | ||
1604 | args.putIntArray("buttonFlags", buttonFlags); | ||
1605 | args.putIntArray("buttonIds", buttonIds); | ||
1606 | args.putStringArray("buttonTexts", buttonTexts); | ||
1607 | args.putIntArray("colors", colors); | ||
1608 | |||
1609 | // trigger Dialog creation on UI thread | ||
1610 | |||
1611 | runOnUiThread(new Runnable() { | ||
1612 | @Override | ||
1613 | public void run() { | ||
1614 | messageboxCreateAndShow(args); | ||
1615 | } | ||
1616 | }); | ||
1617 | |||
1618 | // block the calling thread | ||
1619 | |||
1620 | synchronized (messageboxSelection) { | ||
1621 | try { | ||
1622 | messageboxSelection.wait(); | ||
1623 | } catch (InterruptedException ex) { | ||
1624 | ex.printStackTrace(); | ||
1625 | return -1; | ||
1626 | } | ||
1627 | } | ||
1628 | |||
1629 | // return selected value | ||
1630 | |||
1631 | return messageboxSelection[0]; | ||
1632 | } | ||
1633 | |||
1634 | protected void messageboxCreateAndShow(Bundle args) { | ||
1635 | |||
1636 | // TODO set values from "flags" to messagebox dialog | ||
1637 | |||
1638 | // get colors | ||
1639 | |||
1640 | int[] colors = args.getIntArray("colors"); | ||
1641 | int backgroundColor; | ||
1642 | int textColor; | ||
1643 | int buttonBorderColor; | ||
1644 | int buttonBackgroundColor; | ||
1645 | int buttonSelectedColor; | ||
1646 | if (colors != null) { | ||
1647 | int i = -1; | ||
1648 | backgroundColor = colors[++i]; | ||
1649 | textColor = colors[++i]; | ||
1650 | buttonBorderColor = colors[++i]; | ||
1651 | buttonBackgroundColor = colors[++i]; | ||
1652 | buttonSelectedColor = colors[++i]; | ||
1653 | } else { | ||
1654 | backgroundColor = Color.TRANSPARENT; | ||
1655 | textColor = Color.TRANSPARENT; | ||
1656 | buttonBorderColor = Color.TRANSPARENT; | ||
1657 | buttonBackgroundColor = Color.TRANSPARENT; | ||
1658 | buttonSelectedColor = Color.TRANSPARENT; | ||
1659 | } | ||
1660 | |||
1661 | // create dialog with title and a listener to wake up calling thread | ||
1662 | |||
1663 | final AlertDialog dialog = new AlertDialog.Builder(this).create(); | ||
1664 | dialog.setTitle(args.getString("title")); | ||
1665 | dialog.setCancelable(false); | ||
1666 | dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { | ||
1667 | @Override | ||
1668 | public void onDismiss(DialogInterface unused) { | ||
1669 | synchronized (messageboxSelection) { | ||
1670 | messageboxSelection.notify(); | ||
1671 | } | ||
1672 | } | ||
1673 | }); | ||
1674 | |||
1675 | // create text | ||
1676 | |||
1677 | TextView message = new TextView(this); | ||
1678 | message.setGravity(Gravity.CENTER); | ||
1679 | message.setText(args.getString("message")); | ||
1680 | if (textColor != Color.TRANSPARENT) { | ||
1681 | message.setTextColor(textColor); | ||
1682 | } | ||
1683 | |||
1684 | // create buttons | ||
1685 | |||
1686 | int[] buttonFlags = args.getIntArray("buttonFlags"); | ||
1687 | int[] buttonIds = args.getIntArray("buttonIds"); | ||
1688 | String[] buttonTexts = args.getStringArray("buttonTexts"); | ||
1689 | |||
1690 | final SparseArray<Button> mapping = new SparseArray<Button>(); | ||
1691 | |||
1692 | LinearLayout buttons = new LinearLayout(this); | ||
1693 | buttons.setOrientation(LinearLayout.HORIZONTAL); | ||
1694 | buttons.setGravity(Gravity.CENTER); | ||
1695 | for (int i = 0; i < buttonTexts.length; ++i) { | ||
1696 | Button button = new Button(this); | ||
1697 | final int id = buttonIds[i]; | ||
1698 | button.setOnClickListener(new View.OnClickListener() { | ||
1699 | @Override | ||
1700 | public void onClick(View v) { | ||
1701 | messageboxSelection[0] = id; | ||
1702 | dialog.dismiss(); | ||
1703 | } | ||
1704 | }); | ||
1705 | if (buttonFlags[i] != 0) { | ||
1706 | // see SDL_messagebox.h | ||
1707 | if ((buttonFlags[i] & 0x00000001) != 0) { | ||
1708 | mapping.put(KeyEvent.KEYCODE_ENTER, button); | ||
1709 | } | ||
1710 | if ((buttonFlags[i] & 0x00000002) != 0) { | ||
1711 | mapping.put(KeyEvent.KEYCODE_ESCAPE, button); /* API 11 */ | ||
1712 | } | ||
1713 | } | ||
1714 | button.setText(buttonTexts[i]); | ||
1715 | if (textColor != Color.TRANSPARENT) { | ||
1716 | button.setTextColor(textColor); | ||
1717 | } | ||
1718 | if (buttonBorderColor != Color.TRANSPARENT) { | ||
1719 | // TODO set color for border of messagebox button | ||
1720 | } | ||
1721 | if (buttonBackgroundColor != Color.TRANSPARENT) { | ||
1722 | Drawable drawable = button.getBackground(); | ||
1723 | if (drawable == null) { | ||
1724 | // setting the color this way removes the style | ||
1725 | button.setBackgroundColor(buttonBackgroundColor); | ||
1726 | } else { | ||
1727 | // setting the color this way keeps the style (gradient, padding, etc.) | ||
1728 | drawable.setColorFilter(buttonBackgroundColor, PorterDuff.Mode.MULTIPLY); | ||
1729 | } | ||
1730 | } | ||
1731 | if (buttonSelectedColor != Color.TRANSPARENT) { | ||
1732 | // TODO set color for selected messagebox button | ||
1733 | } | ||
1734 | buttons.addView(button); | ||
1735 | } | ||
1736 | |||
1737 | // create content | ||
1738 | |||
1739 | LinearLayout content = new LinearLayout(this); | ||
1740 | content.setOrientation(LinearLayout.VERTICAL); | ||
1741 | content.addView(message); | ||
1742 | content.addView(buttons); | ||
1743 | if (backgroundColor != Color.TRANSPARENT) { | ||
1744 | content.setBackgroundColor(backgroundColor); | ||
1745 | } | ||
1746 | |||
1747 | // add content to dialog and return | ||
1748 | |||
1749 | dialog.setView(content); | ||
1750 | dialog.setOnKeyListener(new Dialog.OnKeyListener() { | ||
1751 | @Override | ||
1752 | public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) { | ||
1753 | Button button = mapping.get(keyCode); | ||
1754 | if (button != null) { | ||
1755 | if (event.getAction() == KeyEvent.ACTION_UP) { | ||
1756 | button.performClick(); | ||
1757 | } | ||
1758 | return true; // also for ignored actions | ||
1759 | } | ||
1760 | return false; | ||
1761 | } | ||
1762 | }); | ||
1763 | |||
1764 | dialog.show(); | ||
1765 | } | ||
1766 | |||
1767 | private final Runnable rehideSystemUi = new Runnable() { | ||
1768 | @Override | ||
1769 | public void run() { | ||
1770 | if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { | ||
1771 | int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | | ||
1772 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | | ||
1773 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | | ||
1774 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | | ||
1775 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | | ||
1776 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; | ||
1777 | |||
1778 | SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags); | ||
1779 | } | ||
1780 | } | ||
1781 | }; | ||
1782 | |||
1783 | public void onSystemUiVisibilityChange(int visibility) { | ||
1784 | if (SDLActivity.mFullscreenModeActive && ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0)) { | ||
1785 | |||
1786 | Handler handler = getWindow().getDecorView().getHandler(); | ||
1787 | if (handler != null) { | ||
1788 | handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop. | ||
1789 | handler.postDelayed(rehideSystemUi, 2000); | ||
1790 | } | ||
1791 | |||
1792 | } | ||
1793 | } | ||
1794 | |||
1795 | /** | ||
1796 | * This method is called by SDL using JNI. | ||
1797 | */ | ||
1798 | public static boolean clipboardHasText() { | ||
1799 | return mClipboardHandler.clipboardHasText(); | ||
1800 | } | ||
1801 | |||
1802 | /** | ||
1803 | * This method is called by SDL using JNI. | ||
1804 | */ | ||
1805 | public static String clipboardGetText() { | ||
1806 | return mClipboardHandler.clipboardGetText(); | ||
1807 | } | ||
1808 | |||
1809 | /** | ||
1810 | * This method is called by SDL using JNI. | ||
1811 | */ | ||
1812 | public static void clipboardSetText(String string) { | ||
1813 | mClipboardHandler.clipboardSetText(string); | ||
1814 | } | ||
1815 | |||
1816 | /** | ||
1817 | * This method is called by SDL using JNI. | ||
1818 | */ | ||
1819 | public static int createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY) { | ||
1820 | Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); | ||
1821 | ++mLastCursorID; | ||
1822 | |||
1823 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
1824 | try { | ||
1825 | mCursors.put(mLastCursorID, PointerIcon.create(bitmap, hotSpotX, hotSpotY)); | ||
1826 | } catch (Exception e) { | ||
1827 | return 0; | ||
1828 | } | ||
1829 | } else { | ||
1830 | return 0; | ||
1831 | } | ||
1832 | return mLastCursorID; | ||
1833 | } | ||
1834 | |||
1835 | /** | ||
1836 | * This method is called by SDL using JNI. | ||
1837 | */ | ||
1838 | public static void destroyCustomCursor(int cursorID) { | ||
1839 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
1840 | try { | ||
1841 | mCursors.remove(cursorID); | ||
1842 | } catch (Exception e) { | ||
1843 | } | ||
1844 | } | ||
1845 | return; | ||
1846 | } | ||
1847 | |||
1848 | /** | ||
1849 | * This method is called by SDL using JNI. | ||
1850 | */ | ||
1851 | public static boolean setCustomCursor(int cursorID) { | ||
1852 | |||
1853 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
1854 | try { | ||
1855 | mSurface.setPointerIcon(mCursors.get(cursorID)); | ||
1856 | } catch (Exception e) { | ||
1857 | return false; | ||
1858 | } | ||
1859 | } else { | ||
1860 | return false; | ||
1861 | } | ||
1862 | return true; | ||
1863 | } | ||
1864 | |||
1865 | /** | ||
1866 | * This method is called by SDL using JNI. | ||
1867 | */ | ||
1868 | public static boolean setSystemCursor(int cursorID) { | ||
1869 | int cursor_type = 0; //PointerIcon.TYPE_NULL; | ||
1870 | switch (cursorID) { | ||
1871 | case SDL_SYSTEM_CURSOR_ARROW: | ||
1872 | cursor_type = 1000; //PointerIcon.TYPE_ARROW; | ||
1873 | break; | ||
1874 | case SDL_SYSTEM_CURSOR_IBEAM: | ||
1875 | cursor_type = 1008; //PointerIcon.TYPE_TEXT; | ||
1876 | break; | ||
1877 | case SDL_SYSTEM_CURSOR_WAIT: | ||
1878 | cursor_type = 1004; //PointerIcon.TYPE_WAIT; | ||
1879 | break; | ||
1880 | case SDL_SYSTEM_CURSOR_CROSSHAIR: | ||
1881 | cursor_type = 1007; //PointerIcon.TYPE_CROSSHAIR; | ||
1882 | break; | ||
1883 | case SDL_SYSTEM_CURSOR_WAITARROW: | ||
1884 | cursor_type = 1004; //PointerIcon.TYPE_WAIT; | ||
1885 | break; | ||
1886 | case SDL_SYSTEM_CURSOR_SIZENWSE: | ||
1887 | cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; | ||
1888 | break; | ||
1889 | case SDL_SYSTEM_CURSOR_SIZENESW: | ||
1890 | cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; | ||
1891 | break; | ||
1892 | case SDL_SYSTEM_CURSOR_SIZEWE: | ||
1893 | cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; | ||
1894 | break; | ||
1895 | case SDL_SYSTEM_CURSOR_SIZENS: | ||
1896 | cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; | ||
1897 | break; | ||
1898 | case SDL_SYSTEM_CURSOR_SIZEALL: | ||
1899 | cursor_type = 1020; //PointerIcon.TYPE_GRAB; | ||
1900 | break; | ||
1901 | case SDL_SYSTEM_CURSOR_NO: | ||
1902 | cursor_type = 1012; //PointerIcon.TYPE_NO_DROP; | ||
1903 | break; | ||
1904 | case SDL_SYSTEM_CURSOR_HAND: | ||
1905 | cursor_type = 1002; //PointerIcon.TYPE_HAND; | ||
1906 | break; | ||
1907 | case SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT: | ||
1908 | cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; | ||
1909 | break; | ||
1910 | case SDL_SYSTEM_CURSOR_WINDOW_TOP: | ||
1911 | cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; | ||
1912 | break; | ||
1913 | case SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT: | ||
1914 | cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; | ||
1915 | break; | ||
1916 | case SDL_SYSTEM_CURSOR_WINDOW_RIGHT: | ||
1917 | cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; | ||
1918 | break; | ||
1919 | case SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT: | ||
1920 | cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; | ||
1921 | break; | ||
1922 | case SDL_SYSTEM_CURSOR_WINDOW_BOTTOM: | ||
1923 | cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; | ||
1924 | break; | ||
1925 | case SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT: | ||
1926 | cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; | ||
1927 | break; | ||
1928 | case SDL_SYSTEM_CURSOR_WINDOW_LEFT: | ||
1929 | cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; | ||
1930 | break; | ||
1931 | } | ||
1932 | if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { | ||
1933 | try { | ||
1934 | mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type)); | ||
1935 | } catch (Exception e) { | ||
1936 | return false; | ||
1937 | } | ||
1938 | } | ||
1939 | return true; | ||
1940 | } | ||
1941 | |||
1942 | /** | ||
1943 | * This method is called by SDL using JNI. | ||
1944 | */ | ||
1945 | public static void requestPermission(String permission, int requestCode) { | ||
1946 | if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { | ||
1947 | nativePermissionResult(requestCode, true); | ||
1948 | return; | ||
1949 | } | ||
1950 | |||
1951 | Activity activity = (Activity)getContext(); | ||
1952 | if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { | ||
1953 | activity.requestPermissions(new String[]{permission}, requestCode); | ||
1954 | } else { | ||
1955 | nativePermissionResult(requestCode, true); | ||
1956 | } | ||
1957 | } | ||
1958 | |||
1959 | @Override | ||
1960 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { | ||
1961 | boolean result = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED); | ||
1962 | nativePermissionResult(requestCode, result); | ||
1963 | } | ||
1964 | |||
1965 | /** | ||
1966 | * This method is called by SDL using JNI. | ||
1967 | */ | ||
1968 | public static boolean openURL(String url) | ||
1969 | { | ||
1970 | try { | ||
1971 | Intent i = new Intent(Intent.ACTION_VIEW); | ||
1972 | i.setData(Uri.parse(url)); | ||
1973 | |||
1974 | int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; | ||
1975 | if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { | ||
1976 | flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT; | ||
1977 | } else { | ||
1978 | flags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET; | ||
1979 | } | ||
1980 | i.addFlags(flags); | ||
1981 | |||
1982 | mSingleton.startActivity(i); | ||
1983 | } catch (Exception ex) { | ||
1984 | return false; | ||
1985 | } | ||
1986 | return true; | ||
1987 | } | ||
1988 | |||
1989 | /** | ||
1990 | * This method is called by SDL using JNI. | ||
1991 | */ | ||
1992 | public static boolean showToast(String message, int duration, int gravity, int xOffset, int yOffset) | ||
1993 | { | ||
1994 | if(null == mSingleton) { | ||
1995 | return false; | ||
1996 | } | ||
1997 | |||
1998 | try | ||
1999 | { | ||
2000 | class OneShotTask implements Runnable { | ||
2001 | private final String mMessage; | ||
2002 | private final int mDuration; | ||
2003 | private final int mGravity; | ||
2004 | private final int mXOffset; | ||
2005 | private final int mYOffset; | ||
2006 | |||
2007 | OneShotTask(String message, int duration, int gravity, int xOffset, int yOffset) { | ||
2008 | mMessage = message; | ||
2009 | mDuration = duration; | ||
2010 | mGravity = gravity; | ||
2011 | mXOffset = xOffset; | ||
2012 | mYOffset = yOffset; | ||
2013 | } | ||
2014 | |||
2015 | public void run() { | ||
2016 | try | ||
2017 | { | ||
2018 | Toast toast = Toast.makeText(mSingleton, mMessage, mDuration); | ||
2019 | if (mGravity >= 0) { | ||
2020 | toast.setGravity(mGravity, mXOffset, mYOffset); | ||
2021 | } | ||
2022 | toast.show(); | ||
2023 | } catch(Exception ex) { | ||
2024 | Log.e(TAG, ex.getMessage()); | ||
2025 | } | ||
2026 | } | ||
2027 | } | ||
2028 | mSingleton.runOnUiThread(new OneShotTask(message, duration, gravity, xOffset, yOffset)); | ||
2029 | } catch(Exception ex) { | ||
2030 | return false; | ||
2031 | } | ||
2032 | return true; | ||
2033 | } | ||
2034 | |||
2035 | /** | ||
2036 | * This method is called by SDL using JNI. | ||
2037 | */ | ||
2038 | public static int openFileDescriptor(String uri, String mode) throws Exception { | ||
2039 | if (mSingleton == null) { | ||
2040 | return -1; | ||
2041 | } | ||
2042 | |||
2043 | try { | ||
2044 | ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode); | ||
2045 | return pfd != null ? pfd.detachFd() : -1; | ||
2046 | } catch (FileNotFoundException e) { | ||
2047 | e.printStackTrace(); | ||
2048 | return -1; | ||
2049 | } | ||
2050 | } | ||
2051 | |||
2052 | /** | ||
2053 | * This method is called by SDL using JNI. | ||
2054 | */ | ||
2055 | public static boolean showFileDialog(String[] filters, boolean allowMultiple, boolean forWrite, int requestCode) { | ||
2056 | if (mSingleton == null) { | ||
2057 | return false; | ||
2058 | } | ||
2059 | |||
2060 | if (forWrite) { | ||
2061 | allowMultiple = false; | ||
2062 | } | ||
2063 | |||
2064 | /* Convert string list of extensions to their respective MIME types */ | ||
2065 | ArrayList<String> mimes = new ArrayList<>(); | ||
2066 | MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); | ||
2067 | if (filters != null) { | ||
2068 | for (String pattern : filters) { | ||
2069 | String[] extensions = pattern.split(";"); | ||
2070 | |||
2071 | if (extensions.length == 1 && extensions[0].equals("*")) { | ||
2072 | /* Handle "*" special case */ | ||
2073 | mimes.add("*/*"); | ||
2074 | } else { | ||
2075 | for (String ext : extensions) { | ||
2076 | String mime = mimeTypeMap.getMimeTypeFromExtension(ext); | ||
2077 | if (mime != null) { | ||
2078 | mimes.add(mime); | ||
2079 | } | ||
2080 | } | ||
2081 | } | ||
2082 | } | ||
2083 | } | ||
2084 | |||
2085 | /* Display the file dialog */ | ||
2086 | Intent intent = new Intent(forWrite ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_OPEN_DOCUMENT); | ||
2087 | intent.addCategory(Intent.CATEGORY_OPENABLE); | ||
2088 | intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); | ||
2089 | switch (mimes.size()) { | ||
2090 | case 0: | ||
2091 | intent.setType("*/*"); | ||
2092 | break; | ||
2093 | case 1: | ||
2094 | intent.setType(mimes.get(0)); | ||
2095 | break; | ||
2096 | default: | ||
2097 | intent.setType("*/*"); | ||
2098 | intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.toArray(new String[]{})); | ||
2099 | } | ||
2100 | |||
2101 | try { | ||
2102 | mSingleton.startActivityForResult(intent, requestCode); | ||
2103 | } catch (ActivityNotFoundException e) { | ||
2104 | Log.e(TAG, "Unable to open file dialog.", e); | ||
2105 | return false; | ||
2106 | } | ||
2107 | |||
2108 | /* Save current dialog state */ | ||
2109 | mFileDialogState = new SDLFileDialogState(); | ||
2110 | mFileDialogState.requestCode = requestCode; | ||
2111 | mFileDialogState.multipleChoice = allowMultiple; | ||
2112 | return true; | ||
2113 | } | ||
2114 | |||
2115 | /* Internal class used to track active open file dialog */ | ||
2116 | static class SDLFileDialogState { | ||
2117 | int requestCode; | ||
2118 | boolean multipleChoice; | ||
2119 | } | ||
2120 | |||
2121 | /** | ||
2122 | * This method is called by SDL using JNI. | ||
2123 | */ | ||
2124 | public static String getPreferredLocales() { | ||
2125 | String result = ""; | ||
2126 | if (Build.VERSION.SDK_INT >= 24 /* Android 7 (N) */) { | ||
2127 | LocaleList locales = LocaleList.getAdjustedDefault(); | ||
2128 | for (int i = 0; i < locales.size(); i++) { | ||
2129 | if (i != 0) result += ","; | ||
2130 | result += formatLocale(locales.get(i)); | ||
2131 | } | ||
2132 | } else if (mCurrentLocale != null) { | ||
2133 | result = formatLocale(mCurrentLocale); | ||
2134 | } | ||
2135 | return result; | ||
2136 | } | ||
2137 | |||
2138 | public static String formatLocale(Locale locale) { | ||
2139 | String result = ""; | ||
2140 | String lang = ""; | ||
2141 | if (locale.getLanguage() == "in") { | ||
2142 | // Indonesian is "id" according to ISO 639.2, but on Android is "in" because of Java backwards compatibility | ||
2143 | lang = "id"; | ||
2144 | } else if (locale.getLanguage() == "") { | ||
2145 | // Make sure language is never empty | ||
2146 | lang = "und"; | ||
2147 | } else { | ||
2148 | lang = locale.getLanguage(); | ||
2149 | } | ||
2150 | |||
2151 | if (locale.getCountry() == "") { | ||
2152 | result = lang; | ||
2153 | } else { | ||
2154 | result = lang + "_" + locale.getCountry(); | ||
2155 | } | ||
2156 | return result; | ||
2157 | } | ||
2158 | } | ||
2159 | |||
2160 | /** | ||
2161 | Simple runnable to start the SDL application | ||
2162 | */ | ||
2163 | class SDLMain implements Runnable { | ||
2164 | @Override | ||
2165 | public void run() { | ||
2166 | // Runs SDLActivity.main() | ||
2167 | |||
2168 | try { | ||
2169 | android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY); | ||
2170 | } catch (Exception e) { | ||
2171 | Log.v("SDL", "modify thread properties failed " + e.toString()); | ||
2172 | } | ||
2173 | |||
2174 | SDLActivity.nativeInitMainThread(); | ||
2175 | SDLActivity.mSingleton.main(); | ||
2176 | SDLActivity.nativeCleanupMainThread(); | ||
2177 | |||
2178 | if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) { | ||
2179 | // Let's finish the Activity | ||
2180 | SDLActivity.mSDLThread = null; | ||
2181 | SDLActivity.mSDLMainFinished = true; | ||
2182 | SDLActivity.mSingleton.finish(); | ||
2183 | } // else: Activity is already being destroyed | ||
2184 | |||
2185 | } | ||
2186 | } | ||
2187 | |||
2188 | class SDLClipboardHandler implements | ||
2189 | ClipboardManager.OnPrimaryClipChangedListener { | ||
2190 | |||
2191 | protected ClipboardManager mClipMgr; | ||
2192 | |||
2193 | SDLClipboardHandler() { | ||
2194 | mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE); | ||
2195 | mClipMgr.addPrimaryClipChangedListener(this); | ||
2196 | } | ||
2197 | |||
2198 | public boolean clipboardHasText() { | ||
2199 | return mClipMgr.hasPrimaryClip(); | ||
2200 | } | ||
2201 | |||
2202 | public String clipboardGetText() { | ||
2203 | ClipData clip = mClipMgr.getPrimaryClip(); | ||
2204 | if (clip != null) { | ||
2205 | ClipData.Item item = clip.getItemAt(0); | ||
2206 | if (item != null) { | ||
2207 | CharSequence text = item.getText(); | ||
2208 | if (text != null) { | ||
2209 | return text.toString(); | ||
2210 | } | ||
2211 | } | ||
2212 | } | ||
2213 | return null; | ||
2214 | } | ||
2215 | |||
2216 | public void clipboardSetText(String string) { | ||
2217 | mClipMgr.removePrimaryClipChangedListener(this); | ||
2218 | ClipData clip = ClipData.newPlainText(null, string); | ||
2219 | mClipMgr.setPrimaryClip(clip); | ||
2220 | mClipMgr.addPrimaryClipChangedListener(this); | ||
2221 | } | ||
2222 | |||
2223 | @Override | ||
2224 | public void onPrimaryClipChanged() { | ||
2225 | SDLActivity.onNativeClipboardChanged(); | ||
2226 | } | ||
2227 | } | ||
2228 | |||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLAudioManager.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLAudioManager.java new file mode 100644 index 0000000..6ad2f54 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java new file mode 100644 index 0000000..e1c892e --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java new file mode 100644 index 0000000..40e556f --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java new file mode 100644 index 0000000..accce4b --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java b/src/contrib/SDL-3.2.20/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java new file mode 100644 index 0000000..080501c --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..d50bdaa --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png | |||
Binary files differ | |||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..0a299eb --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png | |||
Binary files differ | |||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..a336ad5 --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png | |||
Binary files differ | |||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d423dac --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png | |||
Binary files differ | |||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..959c384 --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png | |||
Binary files differ | |||
diff --git a/src/contrib/SDL-3.2.20/android-project/app/src/main/res/values/colors.xml b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/res/values/strings.xml b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..ab79533 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/app/src/main/res/values/styles.xml b/src/contrib/SDL-3.2.20/android-project/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..7456b1b --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/build.gradle b/src/contrib/SDL-3.2.20/android-project/build.gradle new file mode 100644 index 0000000..ed2299c --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/gradle.properties b/src/contrib/SDL-3.2.20/android-project/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/gradle/wrapper/gradle-wrapper.jar b/src/contrib/SDL-3.2.20/android-project/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..2b338a9 --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/gradle/wrapper/gradle-wrapper.jar | |||
Binary files differ | |||
diff --git a/src/contrib/SDL-3.2.20/android-project/gradle/wrapper/gradle-wrapper.properties b/src/contrib/SDL-3.2.20/android-project/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..99fbfa0 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/gradlew b/src/contrib/SDL-3.2.20/android-project/gradlew new file mode 100755 index 0000000..3427607 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/gradlew.bat b/src/contrib/SDL-3.2.20/android-project/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/src/contrib/SDL-3.2.20/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/src/contrib/SDL-3.2.20/android-project/settings.gradle b/src/contrib/SDL-3.2.20/android-project/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/src/contrib/SDL-3.2.20/android-project/settings.gradle | |||
@@ -0,0 +1 @@ | |||
include ':app' | |||