aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt
blob: c138eadfa279dcadf4f5483c4cfdd23dc254440b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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)
	}
}