diff --git a/android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt b/android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt index 411c8df..27429fa 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt @@ -11,6 +11,9 @@ import java.nio.ByteBuffer import java.nio.ByteOrder import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothProfile +import android.util.Log import android.Manifest import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter @@ -27,6 +30,7 @@ class BleRelayManager( private val callbacks: BleCallbacks ) { companion object { + private const val TAG = "BleRelayManager" private val SERVICE_UUID: UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b") private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8") private val UUID_CHAR_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d9") @@ -55,7 +59,11 @@ class BleRelayManager( override fun onScanResult(callbackType: Int, result: ScanResult) { val device = result.device ?: return val address = device.address ?: return - if (triedAddresses.contains(address)) return + Log.d(TAG, "Found device: ${device.name ?: ""} ($address)") + if (triedAddresses.contains(address)) { + Log.d(TAG, "Skipping already-tried device $address") + return + } stopScan() connectToDevice(device) } @@ -151,7 +159,89 @@ class BleRelayManager( } private val gattCallback = object : android.bluetooth.BluetoothGattCallback() { - // implemented in next task + @SuppressLint("MissingPermission") + override fun onConnectionStateChange(gatt: android.bluetooth.BluetoothGatt, status: Int, newState: Int) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + if (hmacKey == null) { + Log.w(TAG, "GATT connected but no key -- disconnecting") + callbacks.onStatusChanged(BleStatus.NoKey) + gatt.disconnect() + return + } + Log.d(TAG, "GATT connected") + bluetoothGatt = gatt + gatt.discoverServices() + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + Log.d(TAG, "GATT disconnected") + callbacks.onStatusChanged(BleStatus.Disconnected) + gatt.close() + bluetoothGatt = null + relayCharacteristic = null + seq = 1 + handler.postDelayed(scanRunnable, RECONNECT_DELAY_MS) + } + } + + @SuppressLint("MissingPermission") + override fun onServicesDiscovered(gatt: android.bluetooth.BluetoothGatt, status: Int) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.w(TAG, "Service discovery failed (status=$status)") + callbacks.onStatusChanged(BleStatus.ServiceError) + return + } + Log.d(TAG, "Services discovered, requesting MTU...") + gatt.requestMtu(256) + } + + @SuppressLint("MissingPermission") + override fun onMtuChanged(gatt: android.bluetooth.BluetoothGatt, mtu: Int, status: Int) { + Log.d(TAG, "MTU changed to $mtu") + val service = gatt.getService(SERVICE_UUID) + if (service != null) { + relayCharacteristic = service.getCharacteristic(CHARACTERISTIC_UUID) + val uuidChar = service.getCharacteristic(UUID_CHAR_UUID) + if (relayCharacteristic != null && uuidChar != null) { + Log.d(TAG, "Reading device UUID characteristic...") + gatt.readCharacteristic(uuidChar) + } else { + Log.e(TAG, "Service or characteristics not found") + callbacks.onStatusChanged(BleStatus.ServiceNotFound) + } + } else { + Log.e(TAG, "Service not found") + callbacks.onStatusChanged(BleStatus.ServiceNotFound) + } + } + + @SuppressLint("MissingPermission") + override fun onCharacteristicRead(gatt: android.bluetooth.BluetoothGatt, characteristic: android.bluetooth.BluetoothGattCharacteristic, value: ByteArray, status: Int) { + if (characteristic.uuid == UUID_CHAR_UUID && status == BluetoothGatt.GATT_SUCCESS) { + val storedUuid = deviceUuid + if (storedUuid != null && value.contentEquals(storedUuid)) { + Log.d(TAG, "UUID verified, setting up relay") + relayCharacteristic?.let { rc -> + gatt.setCharacteristicNotification(rc, true) + val descriptor = rc.getDescriptor(CCCD_UUID) + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(descriptor) + } + callbacks.onStatusChanged(BleStatus.Connected) + } else { + Log.w(TAG, "UUID mismatch -- disconnecting") + triedAddresses.add(gatt.device.address) + callbacks.onStatusChanged(BleStatus.WrongDevice) + gatt.disconnect() + } + } + } + + @SuppressLint("MissingPermission") + override fun onCharacteristicWrite(gatt: android.bluetooth.BluetoothGatt, characteristic: android.bluetooth.BluetoothGattCharacteristic, status: Int) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.w(TAG, "Write failed (status=$status)") + callbacks.onStatusChanged(BleStatus.CommError) + } + } } private fun loadState() { @@ -161,6 +251,7 @@ class BleRelayManager( val hexUuid = prefs.getString(PrefsHelper.PREF_UUID, null) deviceUuid = hexUuid?.let { hexStringToBytes(it) } seq = prefs.getInt(PrefsHelper.PREF_SEQ, 1).coerceAtMost(PrefsHelper.SEQ_MAX) + Log.d(TAG, "HMAC key loaded: ${hmacKey?.size ?: "none"} bytes, UUID: ${deviceUuid?.size ?: "none"} bytes, seq=$seq") } private fun hexStringToBytes(hex: String): ByteArray? {