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 a29569d..411c8df 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,16 @@ import java.nio.ByteBuffer import java.nio.ByteOrder import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.content.pm.PackageManager +import android.os.ParcelUuid +import androidx.core.app.ActivityCompat class BleRelayManager( private val context: Context, @@ -38,9 +48,92 @@ class BleRelayManager( private var hmacKey: ByteArray? = null private var deviceUuid: ByteArray? = null private var triedAddresses = mutableSetOf() + private val scanRunnable = Runnable { startScan() } + private var scanTimeoutRunnable: Runnable? = null + + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + val device = result.device ?: return + val address = device.address ?: return + if (triedAddresses.contains(address)) return + stopScan() + connectToDevice(device) + } + } + + @SuppressLint("MissingPermission") + fun startScan() { + if (isScanning) return + if (!hasPermissions()) { + callbacks.onStatusChanged(BleStatus.PermissionsDenied) + return + } + val adapter = BluetoothAdapter.getDefaultAdapter() + if (adapter == null || !adapter.isEnabled) { + callbacks.onStatusChanged(BleStatus.BluetoothDisabled) + return + } + loadState() + if (hmacKey == null || deviceUuid == null) { + callbacks.onStatusChanged(BleStatus.NoKey) + return + } + isScanning = true + scanTimedOut = false + scanTimeoutRetries = 0 + callbacks.onStatusChanged(BleStatus.Scanning) + val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build() + val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build() + adapter.bluetoothLeScanner.startScan(listOf(filter), settings, scanCallback) + scanTimeoutRunnable?.let { handler.removeCallbacks(it) } + scanTimeoutRunnable = Runnable { + if (isScanning) { + scanTimedOut = true + stopScan() + } + } + handler.postDelayed(scanTimeoutRunnable!!, SCAN_TIMEOUT_MS) + } + + @SuppressLint("MissingPermission") + fun stopScan() { + if (!isScanning) return + isScanning = false + scanTimeoutRunnable?.let { handler.removeCallbacks(it) } + val adapter = BluetoothAdapter.getDefaultAdapter() + adapter?.bluetoothLeScanner?.stopScan(scanCallback) + if (scanTimedOut) { + if (scanTimeoutRetries < MAX_SCAN_RETRIES) { + scanTimeoutRetries++ + handler.postDelayed(scanRunnable, 2000L) + return + } + callbacks.onStatusChanged(BleStatus.DeviceNotFound) + } + } + + fun hasPermissions(): Boolean { + return getRequiredPermissions().all { + ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED + } + } + + fun getRequiredPermissions(): Array { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + arrayOf( + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.ACCESS_FINE_LOCATION + ) + } else { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) + } + } + + fun destroy() { + handler.removeCallbacksAndMessages(null) + } - fun startScan() {} - fun stopScan() {} fun sendRelayCommand(cmd: Byte) {} fun disconnect() {} fun updateKeys() { @@ -51,11 +144,14 @@ class BleRelayManager( triedAddresses.clear() } - fun hasPermissions(): Boolean = false - fun getRequiredPermissions(): Array = emptyArray() + @SuppressLint("MissingPermission") + private fun connectToDevice(device: android.bluetooth.BluetoothDevice) { + callbacks.onStatusChanged(BleStatus.Connecting) + bluetoothGatt = device.connectGatt(context, false, gattCallback) + } - fun destroy() { - handler.removeCallbacksAndMessages(null) + private val gattCallback = object : android.bluetooth.BluetoothGattCallback() { + // implemented in next task } private fun loadState() {