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