content: 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 /* 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);
      }
  
      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";
      }
  
      @Override
      public UsbDevice getDevice() {
          return null;
      }
  
      @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();
      }
  
  }
  
  