1+ /* *
2+ * SPDX-FileCopyrightText: 2025 microG Project Team
3+ * SPDX-License-Identifier: Apache-2.0
4+ */
5+
6+ package org.microg.gms.fido.core.hybrid.ble
7+
8+ import android.Manifest
9+ import android.annotation.SuppressLint
10+ import android.bluetooth.BluetoothAdapter
11+ import android.bluetooth.le.ScanCallback
12+ import android.bluetooth.le.ScanFilter
13+ import android.bluetooth.le.ScanResult
14+ import android.bluetooth.le.ScanSettings
15+ import android.os.Build
16+ import android.util.Log
17+ import androidx.annotation.RequiresApi
18+ import androidx.annotation.RequiresPermission
19+ import com.google.android.gms.fido.fido2.api.common.ErrorCode
20+ import org.microg.gms.fido.core.RequestHandlingException
21+ import org.microg.gms.fido.core.hybrid.EMPTY_SERVICE_DATA
22+ import org.microg.gms.fido.core.hybrid.EMPTY_SERVICE_DATA_MASK
23+ import org.microg.gms.fido.core.hybrid.UUID_ANDROID
24+ import org.microg.gms.fido.core.hybrid.UUID_IOS
25+ import org.microg.gms.fido.core.hybrid.utils.CryptoHelper
26+ import java.util.concurrent.atomic.AtomicBoolean
27+
28+ private const val TAG = " HybridClientScan"
29+
30+ @RequiresApi(Build .VERSION_CODES .LOLLIPOP )
31+ class HybridClientScan (
32+ private val bluetoothLeAdapter : BluetoothAdapter ? ,
33+ private val seed : ByteArray ,
34+ private val onScanSuccess : (ByteArray ) -> Unit ,
35+ private val onScanFailed : (Throwable ) -> Unit
36+ ) : ScanCallback() {
37+
38+ private val scanStatus = AtomicBoolean (false )
39+
40+ @RequiresPermission(Manifest .permission.BLUETOOTH_SCAN )
41+ override fun onScanResult (callbackType : Int , result : ScanResult ) {
42+ val scanRecord = result.scanRecord
43+ if (scanRecord == null ) {
44+ Log .d(TAG , " processDevice: ScanResult is missing ScanRecord" )
45+ return
46+ }
47+ var serviceData = scanRecord.getServiceData(UUID_ANDROID )
48+ if (serviceData == null ) {
49+ serviceData = scanRecord.getServiceData(UUID_IOS )
50+ }
51+ if (serviceData == null || serviceData.size != 20 ) {
52+ Log .d(TAG , " processDevice: Invalid service data, skipping" )
53+ return
54+ }
55+ Log .d(TAG , " Target device with EID: ${serviceData.joinToString(" " ) { " %02x" .format(it) }} " )
56+ if (CryptoHelper .decryptEid(serviceData, seed) == null ) {
57+ Log .d(TAG , " EID verification failed, not matching current session, continuing scan" )
58+ return
59+ }
60+ stopScanning()
61+ onScanSuccess(serviceData)
62+ }
63+
64+ override fun onScanFailed (errorCode : Int ) {
65+ onScanFailed(RequestHandlingException (ErrorCode .UNKNOWN_ERR , " BLE scan failed: $errorCode " ))
66+ }
67+
68+ @SuppressLint(" MissingPermission" )
69+ fun startScanning () {
70+ if (scanStatus.compareAndSet(false , true )) {
71+ try {
72+ val adapter = bluetoothLeAdapter ? : throw RequestHandlingException (ErrorCode .NOT_SUPPORTED_ERR , " BluetoothAdapter null" )
73+ if (! adapter.isEnabled) {
74+ val enabled = adapter.enable()
75+ if (! enabled) {
76+ throw RequestHandlingException (ErrorCode .NOT_SUPPORTED_ERR , " Unable to enable Bluetooth" )
77+ }
78+ }
79+ val scanner = adapter.bluetoothLeScanner ? : throw RequestHandlingException (ErrorCode .NOT_SUPPORTED_ERR , " BluetoothLeScanner null" )
80+
81+ val filters = listOf (
82+ ScanFilter .Builder ().setServiceData(UUID_ANDROID , EMPTY_SERVICE_DATA , EMPTY_SERVICE_DATA_MASK ).build(),
83+ ScanFilter .Builder ().setServiceData(UUID_IOS , EMPTY_SERVICE_DATA , EMPTY_SERVICE_DATA_MASK ).build()
84+ )
85+ val settings = ScanSettings .Builder ().setScanMode(ScanSettings .SCAN_MODE_LOW_LATENCY ).build()
86+ scanner.startScan(filters, settings, this )
87+ Log .d(TAG , " BLE scanning started" )
88+ } catch (t: Throwable ) {
89+ onScanFailed(RequestHandlingException (ErrorCode .UNKNOWN_ERR , " startScan failed: ${t.message} " ))
90+ }
91+ }
92+ }
93+
94+ @RequiresPermission(Manifest .permission.BLUETOOTH_SCAN )
95+ fun stopScanning () {
96+ if (scanStatus.compareAndSet(true , false )) {
97+ try {
98+ val bluetoothLeScanner = bluetoothLeAdapter?.bluetoothLeScanner
99+ bluetoothLeScanner?.stopScan(this )
100+ Log .d(TAG , " BLE scanning stopped" )
101+ } catch (t: Throwable ) {
102+ onScanFailed(RequestHandlingException (ErrorCode .UNKNOWN_ERR , " stopScan failed: ${t.message} " ))
103+ }
104+ }
105+ }
106+ }
0 commit comments