package com.arslaancodes.zwznfreefit import android.bluetooth.* import android.bluetooth.le.* import android.content.Context import android.os.ParcelUuid import java.util.UUID val SERVICE_UUID = UUID.fromString("6E40FC00-B5A3-F393-E0A9-E50E24DCCA9E") val WRITE_UUID = UUID.fromString("6E40FC20-B5A3-F393-E0A9-E50E24DCCA9E") val NOTIFY_UUID = UUID.fromString("6E40FC21-B5A3-F393-E0A9-E50E24DCCA9E") class BleManager(private val context: Context) { companion object { lateinit var instance: BleManager } init { instance = this } private val adapter = BluetoothAdapter.getDefaultAdapter() private var gatt: BluetoothGatt? = null private var onNotify: ((ByteArray) -> Unit)? = null fun scan(onFound: (BluetoothDevice) -> Unit) { val scanner = adapter.bluetoothLeScanner android.util.Log.d("BLE", "Starting scan, scanner=$scanner") if (scanner == null) { android.util.Log.e("BLE", "Scanner is null! Bluetooth off?") return } val settings = ScanSettings.Builder().build() scanner.startScan(null, settings, object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { android.util.Log.d("BLE", "Found: ${result.device.name} ${result.device.address}") onFound(result.device) } override fun onScanFailed(errorCode: Int) { android.util.Log.e("BLE", "Scan failed: $errorCode") } }) } fun connect(device: BluetoothDevice, onConnected: (BluetoothGatt) -> Unit) { gatt = device.connectGatt(context, false, object : BluetoothGattCallback() { override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { g.discoverServices() } } override fun onServicesDiscovered(g: BluetoothGatt, status: Int) { val notifyChar = g.getService(SERVICE_UUID)?.getCharacteristic(NOTIFY_UUID) if (notifyChar != null) { g.setCharacteristicNotification(notifyChar, true) val descriptor = notifyChar.getDescriptor( UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") ) descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE g.writeDescriptor(descriptor) } onConnected(g) } override fun onCharacteristicChanged(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { if (characteristic.uuid == NOTIFY_UUID) { onNotify?.invoke(characteristic.value) } } }) } fun write(data: ByteArray) { val char = gatt?.getService(SERVICE_UUID)?.getCharacteristic(WRITE_UUID) char?.value = data gatt?.writeCharacteristic(char) } fun syncTime() { val now = System.currentTimeMillis() / 1000 val tzOffset = java.util.TimeZone.getDefault().getOffset(System.currentTimeMillis()) / 1000 val packet = ByteArray(12) packet[0] = 0x01 packet[1] = (now shr 24).toByte() packet[2] = (now shr 16).toByte() packet[3] = (now shr 8).toByte() packet[4] = now.toByte() packet[5] = (tzOffset shr 24).toByte() packet[6] = (tzOffset shr 16).toByte() packet[7] = (tzOffset shr 8).toByte() packet[8] = tzOffset.toByte() packet[9] = 0x00 packet[10] = 0x00 // language code, 0 = english packet[11] = 0x00 write(packet) } fun switchFindBand(flag: Int) { // find/vibrate band // these functions are usually named after their freefit/ferefit counterparts val packet = ByteArray(2) packet[0] = 0x51 // command packet[1] = flag.toByte() // 0x01 = start, 0x00 = stop write(packet) } fun configRealTimeMeasure(type: Int, start: Boolean, onResult: (ByteArray) -> Unit) { // measure heart rate/other stuff // confirmed working types so far is just 0x00 for heart rate, will RE more later // yet again, function named after freefit counterparts from RE'd protocol/decompiled APKs // all code in this file is intellectual property of Arslaan Pathan, no pieces of decompiled freefit code have been used anywhere in the app // freefit decompilation was merely used as a reference for reverse-engineering the protocol onNotify = onResult val packet = ByteArray(3) packet[0] = 0x60.toByte() packet[1] = type.toByte() packet[2] = if (start) 0x01.toByte() else 0x00.toByte() write(packet) } }