From 69db06bcd206ec432fd781f9df8ffd5e07d3ce4d Mon Sep 17 00:00:00 2001
From: Rachel Blackman <[EMAIL REDACTED]>
Date: Tue, 12 May 2026 17:19:18 -0700
Subject: [PATCH] Handle the Amazon Fire TV's weird Bluetooth behavior
(cherry picked from commit 418960bb4e93feaa70f68ec2da7af0f94d08d2dc)
---
.../app/HIDDeviceBLESteamController.java | 134 ++++++++++++++----
1 file changed, 105 insertions(+), 29 deletions(-)
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
index e3dc36cc7d118..e14a11bad1b3e 100644
--- a/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java
+++ b/android-project/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java
@@ -37,6 +37,8 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe
private boolean mIsConnected = false;
private boolean mIsChromebook = false;
private boolean mIsReconnecting = false;
+ private boolean mHasEnabledNotifications = false;
+ private boolean mHasSeenInputUpdate = false;
private boolean mFrozen = false;
private LinkedList<GattOperation> mOperations;
GattOperation mCurrentOperation = null;
@@ -73,6 +75,7 @@ private enum Operation {
byte[] mValue;
BluetoothGatt mGatt;
boolean mResult = true;
+ int mDelayMs = 0;
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
mGatt = gatt;
@@ -80,6 +83,13 @@ private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUI
mUuid = uuid;
}
+ private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) {
+ mGatt = gatt;
+ mOp = operation;
+ mUuid = uuid;
+ mDelayMs = delayMs;
+ }
+
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
mGatt = gatt;
mOp = operation;
@@ -87,6 +97,14 @@ private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUI
mValue = value;
}
+ private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) {
+ mGatt = gatt;
+ mOp = operation;
+ mUuid = uuid;
+ mValue = value;
+ mDelayMs = delayMs;
+ }
+
public void run() {
// This is executed in main thread
BluetoothGattCharacteristic chr;
@@ -148,6 +166,8 @@ public boolean finish() {
return mResult;
}
+ public int getDelayMs() { return mDelayMs; }
+
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
@@ -166,6 +186,10 @@ static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, b
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
}
+
+ static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) {
+ return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs);
+ }
}
HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
@@ -178,6 +202,8 @@ static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
mHandler = new Handler(Looper.getMainLooper());
mGatt = connectGatt();
+ mHasEnabledNotifications = false;
+ mHasSeenInputUpdate = false;
// final HIDDeviceBLESteamController finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
@@ -414,21 +440,30 @@ private void executeNextGattOperation() {
mCurrentOperation = mOperations.removeFirst();
}
- // Run in main thread
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- synchronized (mOperations) {
- if (mCurrentOperation == null) {
- Log.e(TAG, "Current operation null in executor?");
- return;
- }
+ Runnable gattOperationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mOperations) {
+ if (mCurrentOperation == null) {
+ Log.e(TAG, "Current operation null in executor?");
+ return;
+ }
- mCurrentOperation.run();
- // now wait for the GATT callback and when it comes, finish this operation
+ mCurrentOperation.run();
+ // now wait for the GATT callback and when it comes, finish this operation
+ }
}
- }
- });
+ };
+
+ if (mCurrentOperation.getDelayMs() == 0) {
+ // Run in main thread
+ mHandler.post(gattOperationRunnable);
+ }
+ else {
+ // If we have a delay on this operation, wait before we post it.
+ mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs());
+ }
+
}
private void queueGattOperation(GattOperation op) {
@@ -439,8 +474,39 @@ private void queueGattOperation(GattOperation op) {
}
private void enableNotification(UUID chrUuid) {
- GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
+ // Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting
+ // it will return success and then silently drop the operation on the floor.
+ GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500);
queueGattOperation(op);
+
+ // Amazon Fire devices can also silently timeout on writeDescriptor, so
+ // set up a little delayed check that will attempt to write a second time.
+ //
+ // While this only seems to be needed on Amazon Fire TV devices at present, it
+ // doesn't hurt to have a retry on other devices as well.
+ //
+ final HIDDeviceBLESteamController finalThis = this;
+ final UUID finalUuid = chrUuid;
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (!finalThis.mHasEnabledNotifications) {
+
+ if (finalThis.mHasSeenInputUpdate) {
+ // Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen
+ // input reports, though, somewhat by definition notifications are enabled.
+ Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization.");
+ finalThis.mHasEnabledNotifications = true;
+ finalThis.enableValveMode();
+ return;
+ }
+
+ // Give one more try.
+ GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500);
+ finalThis.queueGattOperation(retry);
+ }
+ }
+ }, 1000);
}
void writeCharacteristic(UUID uuid, byte[] value) {
@@ -538,6 +604,7 @@ public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteris
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
if (characteristic.getUuid().equals(getInputCharacteristic()) && !mFrozen) {
+ mHasSeenInputUpdate = true;
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
}
}
@@ -547,27 +614,36 @@ public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descrip
//Log.v(TAG, "onDescriptorRead status=" + status);
}
+ private void enableValveMode()
+ {
+ BluetoothGattService valveService = mGatt.getService(steamControllerService);
+ if (valveService == null)
+ return;
+
+ BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic);
+ if (reportChr != null) {
+ if (getProductId() == TRITON_BLE_PID) {
+ // For Triton we just mark things registered.
+ Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId());
+ mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true);
+ setRegistered();
+ } else {
+ // For the original controller, we need to manually enter Valve mode.
+ Log.v(TAG, "Writing report characteristic to enter valve mode");
+ reportChr.setValue(enterValveMode);
+ mGatt.writeCharacteristic(reportChr);
+ }
+ }
+ }
+
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
if (chr.getUuid().equals(getInputCharacteristic())) {
- boolean hasWrittenInputDescriptor = true;
- BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
- if (reportChr != null) {
- if (getProductId() == TRITON_BLE_PID) {
- // For Triton we just mark things registered.
- Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId());
- mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true);
- setRegistered();
- } else {
- // For the original controller, we need to manually enter Valve mode.
- Log.v(TAG, "Writing report characteristic to enter valve mode");
- reportChr.setValue(enterValveMode);
- gatt.writeCharacteristic(reportChr);
- }
- }
+ mHasEnabledNotifications = true;
+ enableValveMode();
}
finishCurrentGattOperation();