diff options
Diffstat (limited to 'app/src/main')
| -rw-r--r-- | app/src/main/AndroidManifest.xml | 25 | ||||
| -rw-r--r-- | app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt | 121 | ||||
| -rw-r--r-- | app/src/main/java/com/arslaancodes/zwznfreefit/DeviceActivity.kt | 73 | ||||
| -rw-r--r-- | app/src/main/java/com/arslaancodes/zwznfreefit/MainActivity.kt | 75 | ||||
| -rw-r--r-- | app/src/main/res/layout/activity_device.xml | 49 | ||||
| -rw-r--r-- | app/src/main/res/layout/activity_main.xml | 28 | ||||
| -rw-r--r-- | app/src/main/res/values/strings.xml | 3 |
7 files changed, 374 insertions, 0 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..af11ef6 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + + <uses-permission android:name="android.permission.BLUETOOTH" /> + <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> + <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> + <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + + <application + android:allowBackup="true" + android:label="zwzn-freefit" + android:theme="@style/Theme.AppCompat.NoActionBar"> + <activity + android:name=".MainActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + <activity android:name=".DeviceActivity" android:exported="true" android:theme="@style/Theme.AppCompat.NoActionBar"/> + </application> + +</manifest> diff --git a/app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt b/app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt new file mode 100644 index 0000000..c138ead --- /dev/null +++ b/app/src/main/java/com/arslaancodes/zwznfreefit/BleManager.kt @@ -0,0 +1,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) + } +} diff --git a/app/src/main/java/com/arslaancodes/zwznfreefit/DeviceActivity.kt b/app/src/main/java/com/arslaancodes/zwznfreefit/DeviceActivity.kt new file mode 100644 index 0000000..38862bd --- /dev/null +++ b/app/src/main/java/com/arslaancodes/zwznfreefit/DeviceActivity.kt @@ -0,0 +1,73 @@ +package com.arslaancodes.zwznfreefit + +import android.bluetooth.BluetoothAdapter +import android.os.Bundle +import android.widget.TextView +import android.widget.Button +import androidx.appcompat.app.AppCompatActivity + +class DeviceActivity : AppCompatActivity() { + private lateinit var statusText: TextView + private val handler = android.os.Handler(android.os.Looper.getMainLooper()) + private val syncRunnable = object : Runnable { + override fun run() { + BleManager.instance.syncTime() + handler.postDelayed(this, 5 * 60 * 1000) + } + } + + override fun onResume() { + super.onResume() + handler.post(syncRunnable) + } + + override fun onPause() { + super.onPause() + handler.removeCallbacks(syncRunnable) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_device) + window.decorView.systemUiVisibility = ( + android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ) + + val address = intent.getStringExtra("device_address") + val device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address) + + statusText = findViewById(R.id.deviceStatusText) + statusText.text = "Connected to: ${device.name ?: device.address}" + BleManager.instance.syncTime(); + findViewById<Button>(R.id.syncTimeButton).setOnClickListener { + BleManager.instance.syncTime() + } + + findViewById<Button>(R.id.findBandButton).setOnClickListener { + BleManager.instance.switchFindBand(0x01) + } + + findViewById<Button>(R.id.stopFindBandButton).setOnClickListener { + BleManager.instance.switchFindBand(0x00) + } + + findViewById<Button>(R.id.configRealTimeMeasureHeartRateButton).setOnClickListener { + BleManager.instance.configRealTimeMeasure(0x00, true, { data -> + runOnUiThread { + if (data[0] == 0x94.toByte()) { + val heartRate = data[1].toInt() and 0xFF + statusText.text = "Got generic measurement ${heartRate}" + } + if (data[0] == 0xE1.toByte()) { + val rate1 = data[1].toInt() and 0xFF + val rate2 = data[5].toInt() and 0xFF + val rate3 = data[6].toInt() and 0xFF + val rate4 = data[7].toInt() and 0xFF + statusText.text = "Got aggregate rates ${rate1}, ${rate2}, ${rate3}, ${rate4}" + } + } + }) + } + } +} diff --git a/app/src/main/java/com/arslaancodes/zwznfreefit/MainActivity.kt b/app/src/main/java/com/arslaancodes/zwznfreefit/MainActivity.kt new file mode 100644 index 0000000..59bff65 --- /dev/null +++ b/app/src/main/java/com/arslaancodes/zwznfreefit/MainActivity.kt @@ -0,0 +1,75 @@ +package com.arslaancodes.zwznfreefit + +import android.Manifest +import android.bluetooth.BluetoothDevice +import android.content.pm.PackageManager +import android.os.Bundle +import android.widget.* +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import android.content.Intent + +class MainActivity : AppCompatActivity() { + private lateinit var bleManager: BleManager + private lateinit var statusText: TextView + private lateinit var scanButton: Button + private lateinit var deviceList: ListView + + private val devices = mutableListOf<BluetoothDevice>() + private val deviceNames = mutableListOf<String>() + private lateinit var adapter: ArrayAdapter<String> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + window.decorView.systemUiVisibility = ( + android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ) + + bleManager = BleManager(this) + statusText = findViewById(R.id.statusText) + scanButton = findViewById(R.id.scanButton) + deviceList = findViewById(R.id.deviceList) + + adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, deviceNames) + deviceList.adapter = adapter + + deviceList.setOnItemClickListener { _, _, position, _ -> + val device = devices[position] + statusText.text = "Connecting to ${device.name ?: device.address}..." + bleManager.connect(device) { + runOnUiThread { + val intent = Intent(this, DeviceActivity::class.java) + intent.putExtra("device_address", device.address) + startActivity(intent) + } + } + } + + scanButton.setOnClickListener { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + arrayOf( + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.ACCESS_FINE_LOCATION + ), 1) + return@setOnClickListener + } + devices.clear() + deviceNames.clear() + adapter.notifyDataSetChanged() + statusText.text = "Scanning..." + bleManager.scan { device -> + runOnUiThread { + if (devices.none { it.address == device.address }) { + devices.add(device) + deviceNames.add(device.name ?: device.address) + adapter.notifyDataSetChanged() + } + } + } + } + } +} diff --git a/app/src/main/res/layout/activity_device.xml b/app/src/main/res/layout/activity_device.xml new file mode 100644 index 0000000..1364066 --- /dev/null +++ b/app/src/main/res/layout/activity_device.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="24dp" + android:background="#1e1e2f"> + + <TextView + android:id="@+id/deviceStatusText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="18sp"/> + + <Button + android:id="@+id/syncTimeButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Sync Time (syncTime)"/> + + <Button + android:id="@+id/findBandButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Find Watch (switchFindBand 0x01)"/> + + <Button + android:id="@+id/stopFindBandButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Stop Finding Watch (switchFindBand 0x00)"/> + + <Button + android:id="@+id/configRealTimeMeasureHeartRateButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Measure Heart Rate (configRealTimeMeasure type=0x00 start=false)"/> + + <Button + android:id="@+id/stopConfigRealTimeMeasureHeartRateButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Stop Measuring Heart Rate (configRealTimeMeasure type=0x00 start=false)"/> +</LinearLayout> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..e70aa4c --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="#1e1e2f" + android:padding="24dp"> + + <Button + android:id="@+id/scanButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Scan"/> + + <TextView + android:id="@+id/statusText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text=""/> + + <ListView + android:id="@+id/deviceList" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="16dp"/> + +</LinearLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..439573b --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<resources> + <string name="app_name">zwzn-freefit</string> +</resources> |
