From 7b82d4508082d6a20a982a017a2fc3ec021d03dc Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 21 Mar 2026 15:37:07 -0700
Subject: [PATCH] Added android-project template from SDL
---
android-project/app/build.gradle | 63 +
android-project/app/jni/Android.mk | 1 +
android-project/app/jni/Application.mk | 10 +
android-project/app/jni/CMakeLists.txt | 15 +
android-project/app/jni/src/Android.mk | 19 +
android-project/app/jni/src/CMakeLists.txt | 12 +
android-project/app/jni/src/YourSourceHere.c | 26 +
android-project/app/proguard-rules.pro | 77 +
.../app/src/main/AndroidManifest.xml | 108 +
.../main/java/org/libsdl/app/HIDDevice.java | 21 +
.../app/HIDDeviceBLESteamController.java | 655 +++++
.../java/org/libsdl/app/HIDDeviceManager.java | 691 +++++
.../java/org/libsdl/app/HIDDeviceUSB.java | 313 +++
.../app/src/main/java/org/libsdl/app/SDL.java | 90 +
.../main/java/org/libsdl/app/SDLActivity.java | 2230 +++++++++++++++++
.../java/org/libsdl/app/SDLAudioManager.java | 126 +
.../org/libsdl/app/SDLControllerManager.java | 935 +++++++
.../java/org/libsdl/app/SDLDummyEdit.java | 66 +
.../org/libsdl/app/SDLInputConnection.java | 136 +
.../main/java/org/libsdl/app/SDLSurface.java | 449 ++++
.../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2683 bytes
.../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1698 bytes
.../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3872 bytes
.../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6874 bytes
.../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 14526 bytes
.../app/src/main/res/values/colors.xml | 6 +
.../app/src/main/res/values/strings.xml | 3 +
.../app/src/main/res/values/styles.xml | 7 +
android-project/build.gradle | 25 +
android-project/gradle.properties | 17 +
.../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54213 bytes
.../gradle/wrapper/gradle-wrapper.properties | 6 +
android-project/gradlew | 160 ++
android-project/gradlew.bat | 90 +
android-project/settings.gradle | 1 +
35 files changed, 6358 insertions(+)
create mode 100644 android-project/app/build.gradle
create mode 100644 android-project/app/jni/Android.mk
create mode 100644 android-project/app/jni/Application.mk
create mode 100644 android-project/app/jni/CMakeLists.txt
create mode 100644 android-project/app/jni/src/Android.mk
create mode 100644 android-project/app/jni/src/CMakeLists.txt
create mode 100644 android-project/app/jni/src/YourSourceHere.c
create mode 100644 android-project/app/proguard-rules.pro
create mode 100644 android-project/app/src/main/AndroidManifest.xml
create mode 100644 android-project/app/src/main/java/org/libsdl/app/HIDDevice.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/HIDDeviceManager.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDL.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLAudioManager.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLDummyEdit.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLInputConnection.java
create mode 100644 android-project/app/src/main/java/org/libsdl/app/SDLSurface.java
create mode 100644 android-project/app/src/main/res/mipmap-hdpi/ic_launcher.png
create mode 100644 android-project/app/src/main/res/mipmap-mdpi/ic_launcher.png
create mode 100644 android-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png
create mode 100644 android-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
create mode 100644 android-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
create mode 100644 android-project/app/src/main/res/values/colors.xml
create mode 100644 android-project/app/src/main/res/values/strings.xml
create mode 100644 android-project/app/src/main/res/values/styles.xml
create mode 100644 android-project/build.gradle
create mode 100644 android-project/gradle.properties
create mode 100644 android-project/gradle/wrapper/gradle-wrapper.jar
create mode 100644 android-project/gradle/wrapper/gradle-wrapper.properties
create mode 100755 android-project/gradlew
create mode 100644 android-project/gradlew.bat
create mode 100644 android-project/settings.gradle
diff --git a/android-project/app/build.gradle b/android-project/app/build.gradle
new file mode 100644
index 00000000..b9d75b14
--- /dev/null
+++ b/android-project/app/build.gradle
@@ -0,0 +1,63 @@
+plugins {
+ id 'com.android.application'
+}
+
+def buildWithCMake = project.hasProperty('BUILD_WITH_CMAKE');
+
+android {
+ namespace = "org.libsdl.app"
+ compileSdkVersion 35
+ ndkVersion = "28.2.13676358"
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 35
+ versionCode 1
+ versionName "1.0"
+ externalNativeBuild {
+ ndkBuild {
+ arguments "APP_PLATFORM=android-21"
+ // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
+ abiFilters 'arm64-v8a'
+ }
+ cmake {
+ arguments "-DANDROID_PLATFORM=android-21", "-DANDROID_STL=c++_static"
+ // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
+ abiFilters 'arm64-v8a'
+ }
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ applicationVariants.all { variant ->
+ tasks["merge${variant.name.capitalize()}Assets"]
+ .dependsOn("externalNativeBuild${variant.name.capitalize()}")
+ }
+ if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) {
+ sourceSets.main {
+ jniLibs.srcDir 'libs'
+ }
+ externalNativeBuild {
+ if (buildWithCMake) {
+ cmake {
+ path 'jni/CMakeLists.txt'
+ }
+ } else {
+ ndkBuild {
+ path 'jni/Android.mk'
+ }
+ }
+ }
+
+ }
+ lint {
+ abortOnError = false
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+}
diff --git a/android-project/app/jni/Android.mk b/android-project/app/jni/Android.mk
new file mode 100644
index 00000000..5053e7d6
--- /dev/null
+++ b/android-project/app/jni/Android.mk
@@ -0,0 +1 @@
+include $(call all-subdir-makefiles)
diff --git a/android-project/app/jni/Application.mk b/android-project/app/jni/Application.mk
new file mode 100644
index 00000000..1f7c0c10
--- /dev/null
+++ b/android-project/app/jni/Application.mk
@@ -0,0 +1,10 @@
+
+# Uncomment this if you're using STL in your project
+# You can find more information here:
+# https://developer.android.com/ndk/guides/cpp-support
+# APP_STL := c++_shared
+
+APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
+
+# Min runtime API level
+APP_PLATFORM=android-21
diff --git a/android-project/app/jni/CMakeLists.txt b/android-project/app/jni/CMakeLists.txt
new file mode 100644
index 00000000..404b87b3
--- /dev/null
+++ b/android-project/app/jni/CMakeLists.txt
@@ -0,0 +1,15 @@
+cmake_minimum_required(VERSION 3.6)
+
+project(GAME)
+
+# SDL sources are in a subfolder named "SDL"
+add_subdirectory(SDL)
+
+# Compilation of companion libraries
+#add_subdirectory(SDL_image)
+#add_subdirectory(SDL_mixer)
+#add_subdirectory(SDL_ttf)
+
+# Your game and its CMakeLists.txt are in a subfolder named "src"
+add_subdirectory(src)
+
diff --git a/android-project/app/jni/src/Android.mk b/android-project/app/jni/src/Android.mk
new file mode 100644
index 00000000..61672d4f
--- /dev/null
+++ b/android-project/app/jni/src/Android.mk
@@ -0,0 +1,19 @@
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := main
+
+# Add your application source files here...
+LOCAL_SRC_FILES := \
+ YourSourceHere.c
+
+SDL_PATH := ../SDL # SDL
+
+LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include # SDL
+
+LOCAL_SHARED_LIBRARIES := SDL3
+
+LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid # SDL
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/android-project/app/jni/src/CMakeLists.txt b/android-project/app/jni/src/CMakeLists.txt
new file mode 100644
index 00000000..02128376
--- /dev/null
+++ b/android-project/app/jni/src/CMakeLists.txt
@@ -0,0 +1,12 @@
+cmake_minimum_required(VERSION 3.6)
+
+project(my_app)
+
+if(NOT TARGET SDL3::SDL3)
+ find_package(SDL3 CONFIG REQUIRED)
+endif()
+
+add_library(main SHARED
+ YourSourceHere.c
+)
+target_link_libraries(main PRIVATE SDL3::SDL3)
diff --git a/android-project/app/jni/src/YourSourceHere.c b/android-project/app/jni/src/YourSourceHere.c
new file mode 100644
index 00000000..87b82973
--- /dev/null
+++ b/android-project/app/jni/src/YourSourceHere.c
@@ -0,0 +1,26 @@
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */
+/* */
+/* Remove this source, and replace with your SDL sources */
+/* */
+/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */
+
+int main(int argc, char *argv[]) {
+ (void)argc;
+ (void)argv;
+ if (!SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO)) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed (%s)", SDL_GetError());
+ return 1;
+ }
+
+ if (!SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Hello World",
+ "!! Your SDL project successfully runs on Android !!", NULL)) {
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_ShowSimpleMessageBox failed (%s)", SDL_GetError());
+ return 1;
+ }
+
+ SDL_Quit();
+ return 0;
+}
diff --git a/android-project/app/proguard-rules.pro b/android-project/app/proguard-rules.pro
new file mode 100644
index 00000000..0fb7ae09
--- /dev/null
+++ b/android-project/app/proguard-rules.pro
@@ -0,0 +1,77 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in [sdk]/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# https://developer.android.com/build/shrink-code
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity {
+ 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
+ java.lang.String clipboardGetText();
+ boolean clipboardHasText();
+ void clipboardSetText(java.lang.String);
+ int createCustomCursor(int[], int, int, int, int);
+ void destroyCustomCursor(int);
+ android.app.Activity getContext();
+ boolean getManifestEnvironmentVariables();
+ android.view.Surface getNativeSurface();
+ void initTouch();
+ boolean isAndroidTV();
+ boolean isChromebook();
+ boolean isDeXMode();
+ boolean isTablet();
+ void manualBackButton();
+ int messageboxShowMessageBox(int, java.lang.String, java.lang.String, int[], int[], java.lang.String[], int[]);
+ void minimizeWindow();
+ boolean openURL(java.lang.String);
+ void requestPermission(java.lang.String, int);
+ boolean showToast(java.lang.String, int, int, int, int);
+ boolean sendMessage(int, int);
+ boolean setActivityTitle(java.lang.String);
+ boolean setCustomCursor(int);
+ void setOrientation(int, int, boolean, java.lang.String);
+ boolean setRelativeMouseEnabled(boolean);
+ boolean setSystemCursor(int);
+ void setWindowStyle(boolean);
+ boolean shouldMinimizeOnFocusLoss();
+ boolean showTextInput(int, int, int, int, int);
+ boolean supportsRelativeMouse();
+ int openFileDescriptor(java.lang.String, java.lang.String);
+ boolean showFileDialog(java.lang.String[], boolean, boolean, int);
+ java.lang.String getPreferredLocales();
+ java.lang.String formatLocale(java.util.Locale);
+}
+
+-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.HIDDeviceManager {
+ void closeDevice(int);
+ boolean initialize(boolean, boolean);
+ boolean openDevice(int);
+ boolean readReport(int, byte[], boolean);
+ int writeReport(int, byte[], boolean);
+}
+
+-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLAudioManager {
+ void registerAudioDeviceCallback();
+ void unregisterAudioDeviceCallback();
+ void audioSetThreadPriority(boolean, int);
+}
+
+-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLControllerManager {
+ void pollInputDevices();
+ void joystickSetLED(int, int, int, int);
+ void pollHapticDevices();
+ void hapticRun(int, float, int);
+ void hapticRumble(int, float, float, int);
+ void hapticStop(int);
+}
diff --git a/android-project/app/src/main/AndroidManifest.xml b/android-project/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..ab43f76a
--- /dev/null
+++ b/android-project/app/src/main/AndroidManifest.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ android:versionCode="1"
+ android:versionName="1.0"
+ android:installLocation="auto">
+
+ <!-- OpenGL ES 2.0 -->
+ <uses-feature android:glEsVersion="0x00020000" />
+
+ <!-- Touchscreen support -->
+ <uses-feature
+ android:name="android.hardware.touchscreen"
+ android:required="false" />
+
+ <!-- Game controller support -->
+ <uses-feature
+ android:name="android.hardware.bluetooth"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.gamepad"
+ android:required="false" />
+ <uses-feature
+ android:name="android.hardware.usb.host"
+ android:required="false" />
+
+ <!-- External mouse input events -->
+ <uses-feature
+ android:name="android.hardware.type.pc"
+ android:required="false" />
+
+ <!-- Audio recording support -->
+ <!-- if you want to record audio, uncomment this. -->
+ <!-- <uses-permission android:name="android.permission.RECORD_AUDIO" /> -->
+ <!-- <uses-feature
+ android:name="android.hardware.microphone"
+ android:required="false" /> -->
+
+ <!-- Camera support -->
+ <!-- if you want to record video, uncomment this. -->
+ <!--
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-feature android:name="android.hardware.camera" />
+ -->
+
+ <!-- Allow downloading to the external storage on Android 5.1 and older -->
+ <!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> -->
+
+ <!-- Allow access to Bluetooth devices -->
+ <!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM -->
+ <!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> -->
+ <!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> -->
+
+ <!-- Allow access to the vibrator -->
+ <uses-permission android:name="android.permission.VIBRATE" />
+
+ <!-- Allow access to Internet -->
+ <!-- if you want to connect to the network or internet, uncomment this. -->
+ <!--
+ <uses-permission android:name="android.permission.INTERNET" />
+ -->
+
+ <!-- Create a Java class extending SDLActivity and place it in a
+ directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java
+
+ then replace "SDLActivity" with the name of your class (e.g. "MyGame")
+ in the XML below.
+
+ An example Java class can be found in README-android.md
+ -->
+ <application android:label="@string/app_name"
+ android:icon="@mipmap/ic_launcher"
+ android:allowBackup="true"
+ android:theme="@style/AppTheme"
+ android:enableOnBackInvokedCallback="false"
+ android:hardwareAccelerated="true" >
+
+ <!-- Example of setting SDL hints from AndroidManifest.xml:
+ <meta-data android:name="SDL_ENV.SDL_ANDROID_TRAP_BACK_BUTTON" android:value="0"/>
+ -->
+
+ <activity android:name="SDLActivity"
+ android:label="@string/app_name"
+ android:alwaysRetainTaskState="true"
+ android:launchMode="singleInstance"
+ android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
+ android:preferMinimalPostProcessing="true"
+ android:exported="true"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <!-- Let Android know that we can handle some USB devices and should receive this event -->
+ <intent-filter>
+ <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
+ </intent-filter>
+ <!-- Drop file event -->
+ <!--
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="*/*" />
+ </intent-filter>
+ -->
+ </activity>
+ </application>
+
+</manifest>
diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java b/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java
new file mode 100644
index 00000000..f9609532
--- /dev/null
+++ b/android-project/app/src/main/java/org/libsdl/app/HIDDevice.java
@@ -0,0 +1,21 @@
+package org.libsdl.app;
+
+import android.hardware.usb.UsbDevice;
+
+interface HIDDevice
+{
+ public int getId();
+ public int getVendorId();
+ public int getProductId();
+ public String getSerialNumber();
+ public int getVersion();
+ public String getManufacturerName();
+ public String getProductName();
+ public UsbDevice getDevice();
+ public boolean open();
+ public int writeReport(byte[] report, boolean feature);
+ public boolean readReport(byte[] report, boolean feature);
+ public void setFrozen(boolean frozen);
+ public void close();
+ public void shutdown();
+}
diff --git a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java
new file mode 100644
index 00000000..bf1ca214
--- /dev/null
+++ b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java
@@ -0,0 +1,655 @@
+package org.libsdl.app;
+
+import android.content.Context;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothGattService;
+import android.hardware.usb.UsbDevice;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.os.*;
+
+//import com.android.internal.util.HexDump;
+
+import java.lang.Runnable;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.UUID;
+
+class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
+
+ private static final String TAG = "hidapi";
+ private HIDDeviceManager mManager;
+ private BluetoothDevice mDevice;
+ private int mDeviceId;
+ private BluetoothGatt mGatt;
+ private boolean mIsRegistered = false;
+ private boolean mIsConnected = false;
+ private boolean mIsChromebook = false;
+ private boolean mIsReconnecting = false;
+ private boolean mFrozen = false;
+ private LinkedList<GattOperation> mOperations;
+ GattOperation mCurrentOperation = null;
+ private Handler mHandler;
+
+ private static final int TRANSPORT_AUTO = 0;
+ private static final int TRANSPORT_BREDR = 1;
+ private static final int TRANSPORT_LE = 2;
+
+ private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
+
+ static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
+ static final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
+ static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
+ static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
+
+ static class GattOperation {
+ private enum Operation {
+ CHR_READ,
+ CHR_WRITE,
+ ENABLE_NOTIFICATION
+ }
+
+ Operation mOp;
+ UUID mUuid;
+ byte[] mValue;
+ BluetoothGatt mGatt;
+ boolean mResult = true;
+
+ private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
+ mGatt = gatt;
+ mOp = operation;
+ mUuid = uuid;
+ }
+
+ private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
+ mGatt = gatt;
+ mOp = operation;
+ mUuid = uuid;
+ mValue = value;
+ }
+
+ public void run() {
+ // This is executed in main thread
+ BluetoothGattCharacteristic chr;
+
+ switch (mOp) {
+ case CHR_READ:
+ chr = getCharacteristic(mUuid);
+ //Log.v(TAG, "Reading characteristic " + chr.getUuid());
+ if (!mGatt.readCharacteristic(chr)) {
+ Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
+ mResult = false;
+ break;
+ }
+ mResult = true;
+ break;
+ case CHR_WRITE:
+ chr = getCharacteristic(mUuid);
+ //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
+ chr.setValue(mValue);
+ if (!mGatt.writeCharacteristic(chr)) {
+ Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
+ mResult = false;
+ break;
+ }
+ mResult = true;
+ break;
+ case ENABLE_NOTIFICATION:
+ chr = getCharacteristic(mUuid);
+ //Log.v(TAG, "Writing descriptor of " + chr.getUuid());
+ if (chr != null) {
+ BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
+ if (cccd != null) {
+ int properties = chr.getProperties();
+ byte[] value;
+ if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
+ value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
+ } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
+ value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
+ } else {
+ Log.e(TAG, "Unable to start notifications on input characteristic");
+ mResult = false;
+ return;
+ }
+
+ mGatt.setCharacteristicNotification(chr, true);
+ cccd.setValue(value);
+ if (!mGatt.writeDescriptor(cccd)) {
+ Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
+ mResult = false;
+ return;
+ }
+ mResult = true;
+ }
+ }
+ }
+ }
+
+ public boolean finish() {
+ return mResult;
+ }
+
+ private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
+ BluetoothGattService valveService = mGatt.getService(steamControllerService);
+ if (valveService == null)
+ return null;
+ return valveService.getCharacteristic(uuid);
+ }
+
+ static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
+ return new GattOperation(gatt, Operation.CHR_READ, uuid);
+ }
+
+ static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
+ return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
+ }
+
+ static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
+ return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
+ }
+ }
+
+ HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
+ mManager = manager;
+ mDevice = device;
+ mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
+ mIsRegistered = false;
+ mIsChromebook = SDLActivity.isChromebook();
+ mOperations = new LinkedList<GattOperation>();
+ mHandler = new Handler(Looper.getMainLooper());
+
+ mGatt = connectGatt();
+ // final HIDDeviceBLESteamController finalThis = this;
+ // mHandler.postDelayed(new Runnable() {
+ // @Override
+ // void run() {
+ // finalThis.checkConnectionForChromebookIssue();
+ // }
+ // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
+ }
+
+ String getIdentifier() {
+ return String.format("SteamController.%s", mDevice.getAddress());
+ }
+
+ BluetoothGatt getGatt() {
+ return mGatt;
+ }
+
+ // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
+ // of TRANSPORT_LE. Let's force ourselves to connect low energy.
+ private BluetoothGatt connectGatt(boolean managed) {
+ if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
+ try {
+ return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
+ } catch (Exception e) {
+ return mDevice.connectGatt(mManager.getContext(), managed, this);
+ }
+ } else {
+ return mDevice.connectGatt(mManager.getContext(), managed, this);
+ }
+ }
+
+ private BluetoothGatt connectGatt() {
+ return connectGatt(false);
+ }
+
+ protected int getConnectionState() {
+
+ Context context = mManager.getContext();
+ if (context == null) {
+ // We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
+ if (btManager == null) {
+ // This device doesn't support Bluetooth. We should never be here, because how did
+ // we instantiate a device to start with?
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
+ }
+
+ void reconnect() {
+
+ if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
+ mGatt.disconnect();
+ mGatt = connectGatt();
+ }
+
+ }
+
+ protected void checkConnectionForChromebookIssue() {
+ if (!mIsChromebook) {
+ // We only do this on Chromebooks, because otherwise it's really annoying to just attempt
+ // over and over.
+ return;
+ }
+
+ int connectionState = getConnectionState();
+
+ switch (connectionState) {
+ case BluetoothProfile.STATE_CONNECTED:
+ if (!mIsConnected) {
+ // We are in the Bad Chromebook Place. We can force a disconnect
+ // to try to recover.
+ 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.");
+ mIsReconnecting = true;
+ mGatt.disconnect();
+ mGatt = connectGatt(false);
+ break;
+ }
+ else if (!isRegistered()) {
+ if (mGatt.getServices().size() > 0) {
+ Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
+ probeService(this);
+ }
+ else {
+ Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
+ mIsReconnecting = true;
+ mGatt.disconnect();
+ mGatt = connectGatt(false);
+ break;
+ }
+ }
+ else {
+ Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
+ return;
+ }
+ break;
+
+ case BluetoothProfile.STATE_DISCONNECTED:
+ 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.");
+
+ mIsReconnecting = true;
+ mGatt.disconnect();
+ mGatt = connectGatt(false);
+ break;
+
+ case BluetoothProfile.STATE_CONNECTING:
+ Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
+ break;
+ }
+
+ final HIDDeviceBLESteamController finalThis = this;
+ mHandler.postDelayed(new Runnable() {
+ @Override
(Patch may be truncated, please check the link at the top of this post.)