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