2021-04-18 07:35:25 +04:00
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 public final UUID steamControllerService = UUID . fromString ( " 100F6C32-1735-4313-B402-38567131E5F3 " ) ;
static public final UUID inputCharacteristic = UUID . fromString ( " 100F6C33-1735-4313-B402-38567131E5F3 " ) ;
static public 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 ) ;
}
}
public HIDDeviceBLESteamController ( HIDDeviceManager manager , BluetoothDevice device ) {
mManager = manager ;
mDevice = device ;
mDeviceId = mManager . getDeviceIDForIdentifier ( getIdentifier ( ) ) ;
mIsRegistered = false ;
mIsChromebook = mManager . getContext ( ) . getPackageManager ( ) . hasSystemFeature ( " org.chromium.arc.device_management " ) ;
mOperations = new LinkedList < GattOperation > ( ) ;
mHandler = new Handler ( Looper . getMainLooper ( ) ) ;
mGatt = connectGatt ( ) ;
// final HIDDeviceBLESteamController finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.checkConnectionForChromebookIssue();
// }
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
public String getIdentifier ( ) {
return String . format ( " SteamController.%s " , mDevice . getAddress ( ) ) ;
}
public 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 ) {
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 ) ;
}
public 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
public void run ( ) {
finalThis . checkConnectionForChromebookIssue ( ) ;
}
} , CHROMEBOOK_CONNECTION_CHECK_INTERVAL ) ;
}
private boolean isRegistered ( ) {
return mIsRegistered ;
}
private void setRegistered ( ) {
mIsRegistered = true ;
}
private boolean probeService ( HIDDeviceBLESteamController controller ) {
if ( isRegistered ( ) ) {
return true ;
}
if ( ! mIsConnected ) {
return false ;
}
Log . v ( TAG , " probeService controller= " + controller ) ;
for ( BluetoothGattService service : mGatt . getServices ( ) ) {
if ( service . getUuid ( ) . equals ( steamControllerService ) ) {
Log . v ( TAG , " Found Valve steam controller service " + service . getUuid ( ) ) ;
for ( BluetoothGattCharacteristic chr : service . getCharacteristics ( ) ) {
if ( chr . getUuid ( ) . equals ( inputCharacteristic ) ) {
Log . v ( TAG , " Found input characteristic " ) ;
// Start notifications
BluetoothGattDescriptor cccd = chr . getDescriptor ( UUID . fromString ( " 00002902-0000-1000-8000-00805f9b34fb " ) ) ;
if ( cccd ! = null ) {
enableNotification ( chr . getUuid ( ) ) ;
}
}
}
return true ;
}
}
if ( ( mGatt . getServices ( ) . size ( ) = = 0 ) & & mIsChromebook & & ! mIsReconnecting ) {
Log . e ( TAG , " Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us. " ) ;
mIsConnected = false ;
mIsReconnecting = true ;
mGatt . disconnect ( ) ;
mGatt = connectGatt ( false ) ;
}
return false ;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private void finishCurrentGattOperation ( ) {
GattOperation op = null ;
synchronized ( mOperations ) {
if ( mCurrentOperation ! = null ) {
op = mCurrentOperation ;
mCurrentOperation = null ;
}
}
if ( op ! = null ) {
boolean result = op . finish ( ) ; // TODO: Maybe in main thread as well?
// Our operation failed, let's add it back to the beginning of our queue.
if ( ! result ) {
mOperations . addFirst ( op ) ;
}
}
executeNextGattOperation ( ) ;
}
private void executeNextGattOperation ( ) {
synchronized ( mOperations ) {
if ( mCurrentOperation ! = null )
return ;
if ( mOperations . isEmpty ( ) )
return ;
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 ;
}
mCurrentOperation . run ( ) ;
// now wait for the GATT callback and when it comes, finish this operation
}
}
} ) ;
}
private void queueGattOperation ( GattOperation op ) {
synchronized ( mOperations ) {
mOperations . add ( op ) ;
}
executeNextGattOperation ( ) ;
}
private void enableNotification ( UUID chrUuid ) {
GattOperation op = HIDDeviceBLESteamController . GattOperation . enableNotification ( mGatt , chrUuid ) ;
queueGattOperation ( op ) ;
}
public void writeCharacteristic ( UUID uuid , byte [ ] value ) {
GattOperation op = HIDDeviceBLESteamController . GattOperation . writeCharacteristic ( mGatt , uuid , value ) ;
queueGattOperation ( op ) ;
}
public void readCharacteristic ( UUID uuid ) {
GattOperation op = HIDDeviceBLESteamController . GattOperation . readCharacteristic ( mGatt , uuid ) ;
queueGattOperation ( op ) ;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////////// BluetoothGattCallback overridden methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
public void onConnectionStateChange ( BluetoothGatt g , int status , int newState ) {
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
mIsReconnecting = false ;
if ( newState = = 2 ) {
mIsConnected = true ;
// Run directly, without GattOperation
if ( ! isRegistered ( ) ) {
mHandler . post ( new Runnable ( ) {
@Override
public void run ( ) {
mGatt . discoverServices ( ) ;
}
} ) ;
}
}
else if ( newState = = 0 ) {
mIsConnected = false ;
}
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
}
public void onServicesDiscovered ( BluetoothGatt gatt , int status ) {
//Log.v(TAG, "onServicesDiscovered status=" + status);
if ( status = = 0 ) {
if ( gatt . getServices ( ) . size ( ) = = 0 ) {
Log . v ( TAG , " onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack. " ) ;
mIsReconnecting = true ;
mIsConnected = false ;
gatt . disconnect ( ) ;
mGatt = connectGatt ( false ) ;
}
else {
probeService ( this ) ;
}
}
}
public void onCharacteristicRead ( BluetoothGatt gatt , BluetoothGattCharacteristic characteristic , int status ) {
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
if ( characteristic . getUuid ( ) . equals ( reportCharacteristic ) & & ! mFrozen ) {
mManager . HIDDeviceFeatureReport ( getId ( ) , characteristic . getValue ( ) ) ;
}
finishCurrentGattOperation ( ) ;
}
public void onCharacteristicWrite ( BluetoothGatt gatt , BluetoothGattCharacteristic characteristic , int status ) {
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
if ( characteristic . getUuid ( ) . equals ( reportCharacteristic ) ) {
// Only register controller with the native side once it has been fully configured
if ( ! isRegistered ( ) ) {
Log . v ( TAG , " Registering Steam Controller with ID: " + getId ( ) ) ;
mManager . HIDDeviceConnected ( getId ( ) , getIdentifier ( ) , getVendorId ( ) , getProductId ( ) , getSerialNumber ( ) , getVersion ( ) , getManufacturerName ( ) , getProductName ( ) , 0 , 0 , 0 , 0 ) ;
setRegistered ( ) ;
}
}
finishCurrentGattOperation ( ) ;
}
public void onCharacteristicChanged ( BluetoothGatt gatt , BluetoothGattCharacteristic characteristic ) {
// Enable this for verbose logging of controller input reports
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
if ( characteristic . getUuid ( ) . equals ( inputCharacteristic ) & & ! mFrozen ) {
mManager . HIDDeviceInputReport ( getId ( ) , characteristic . getValue ( ) ) ;
}
}
public void onDescriptorRead ( BluetoothGatt gatt , BluetoothGattDescriptor descriptor , int status ) {
//Log.v(TAG, "onDescriptorRead status=" + status);
}
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 ( inputCharacteristic ) ) {
boolean hasWrittenInputDescriptor = true ;
BluetoothGattCharacteristic reportChr = chr . getService ( ) . getCharacteristic ( reportCharacteristic ) ;
if ( reportChr ! = null ) {
Log . v ( TAG , " Writing report characteristic to enter valve mode " ) ;
reportChr . setValue ( enterValveMode ) ;
gatt . writeCharacteristic ( reportChr ) ;
}
}
finishCurrentGattOperation ( ) ;
}
public void onReliableWriteCompleted ( BluetoothGatt gatt , int status ) {
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
}
public void onReadRemoteRssi ( BluetoothGatt gatt , int rssi , int status ) {
//Log.v(TAG, "onReadRemoteRssi status=" + status);
}
public void onMtuChanged ( BluetoothGatt gatt , int mtu , int status ) {
//Log.v(TAG, "onMtuChanged status=" + status);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////// Public API
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int getId ( ) {
return mDeviceId ;
}
@Override
public int getVendorId ( ) {
// Valve Corporation
final int VALVE_USB_VID = 0x28DE ;
return VALVE_USB_VID ;
}
@Override
public int getProductId ( ) {
// We don't have an easy way to query from the Bluetooth device, but we know what it is
final int D0G_BLE2_PID = 0x1106 ;
return D0G_BLE2_PID ;
}
@Override
public String getSerialNumber ( ) {
// This will be read later via feature report by Steam
return " 12345 " ;
}
@Override
public int getVersion ( ) {
return 0 ;
}
@Override
public String getManufacturerName ( ) {
return " Valve Corporation " ;
}
@Override
public String getProductName ( ) {
return " Steam Controller " ;
}
2021-12-07 05:20:09 +04:00
@Override
2021-04-18 07:35:25 +04:00
public UsbDevice getDevice ( ) {
2021-12-07 05:20:09 +04:00
return null ;
}
2021-04-18 07:35:25 +04:00
@Override
public boolean open ( ) {
return true ;
}
@Override
public int sendFeatureReport ( byte [ ] report ) {
if ( ! isRegistered ( ) ) {
Log . e ( TAG , " Attempted sendFeatureReport before Steam Controller is registered! " ) ;
if ( mIsConnected ) {
probeService ( this ) ;
}
return - 1 ;
}
// We need to skip the first byte, as that doesn't go over the air
byte [ ] actual_report = Arrays . copyOfRange ( report , 1 , report . length - 1 ) ;
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
writeCharacteristic ( reportCharacteristic , actual_report ) ;
return report . length ;
}
@Override
public int sendOutputReport ( byte [ ] report ) {
if ( ! isRegistered ( ) ) {
Log . e ( TAG , " Attempted sendOutputReport before Steam Controller is registered! " ) ;
if ( mIsConnected ) {
probeService ( this ) ;
}
return - 1 ;
}
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
writeCharacteristic ( reportCharacteristic , report ) ;
return report . length ;
}
@Override
public boolean getFeatureReport ( byte [ ] report ) {
if ( ! isRegistered ( ) ) {
Log . e ( TAG , " Attempted getFeatureReport before Steam Controller is registered! " ) ;
if ( mIsConnected ) {
probeService ( this ) ;
}
return false ;
}
//Log.v(TAG, "getFeatureReport");
readCharacteristic ( reportCharacteristic ) ;
return true ;
}
@Override
public void close ( ) {
}
@Override
public void setFrozen ( boolean frozen ) {
mFrozen = frozen ;
}
@Override
public void shutdown ( ) {
close ( ) ;
BluetoothGatt g = mGatt ;
if ( g ! = null ) {
g . disconnect ( ) ;
g . close ( ) ;
mGatt = null ;
}
mManager = null ;
mIsRegistered = false ;
mIsConnected = false ;
mOperations . clear ( ) ;
}
}