[+] Scan activity

This commit is contained in:
Azalea Gui
2023-01-23 14:56:17 -05:00
parent 032423fa4d
commit e867a080b5
4 changed files with 160 additions and 69 deletions
@@ -1,32 +1,40 @@
package org.hydev.wearsync.bles package com.welie.blessedexample
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.content.* import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.LocationManager import android.location.LocationManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.view.View import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.welie.blessed.BluetoothPeripheral import com.welie.blessed.BluetoothPeripheral
import org.hydev.wearsync.bles.BluetoothHandler.Companion.getInstance
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import org.hydev.wearsync.R import org.hydev.wearsync.bles.BluetoothHandler
import org.hydev.wearsync.bles.ObservationUnit
import org.hydev.wearsync.databinding.ActivityScanBinding
import org.hydev.wearsync.snack
import timber.log.Timber import timber.log.Timber
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class MainActivity : AppCompatActivity() { class ActivityScan : AppCompatActivity() {
lateinit var binding: ActivityScanBinding
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var measurementValue: TextView? = null
private val dateFormat: DateFormat = SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.ENGLISH) private val dateFormat: DateFormat = SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.ENGLISH)
private val enableBluetoothRequest = private val enableBluetoothRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -40,21 +48,22 @@ class MainActivity : AppCompatActivity() {
} }
private val bluetoothManager by lazy { private val bluetoothManager by lazy {
applicationContext applicationContext.getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
.getSystemService(BLUETOOTH_SERVICE)
as BluetoothManager
} }
private lateinit var bluetoothHandler: BluetoothHandler
private fun askToEnableBluetooth() { private fun askToEnableBluetooth() {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
enableBluetoothRequest.launch(enableBtIntent) enableBluetoothRequest.launch(enableBtIntent)
} }
@SuppressLint("MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) binding = ActivityScanBinding.inflate(layoutInflater)
measurementValue = findViewById<View>(R.id.bloodPressureValue) as TextView setContentView(binding.root)
registerReceiver( registerReceiver(
locationServiceStateReceiver, locationServiceStateReceiver,
IntentFilter(LocationManager.MODE_CHANGED_ACTION) IntentFilter(LocationManager.MODE_CHANGED_ACTION)
@@ -64,74 +73,86 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (bluetoothManager.adapter != null) { if (bluetoothManager.adapter != null) {
if (!isBluetoothEnabled) { if (!isBluetoothEnabled) askToEnableBluetooth() else checkPermissions()
askToEnableBluetooth()
} else {
checkPermissions()
}
} else { } else {
Timber.e("This device has no Bluetooth hardware") Timber.e("This device has no Bluetooth hardware")
} }
} }
private val isBluetoothEnabled: Boolean private val isBluetoothEnabled: Boolean
get() { get() = bluetoothManager.adapter?.isEnabled ?: false
val bluetoothAdapter = bluetoothManager.adapter ?: return false
return bluetoothAdapter.isEnabled private val central get() = bluetoothHandler.central
@SuppressLint("MissingPermission")
private fun initBluetoothHandler() {
if (this::bluetoothHandler.isInitialized) return
bluetoothHandler = BluetoothHandler.getInstance(applicationContext)
println("OnCreate called, Initializing...")
// List bonded device addresses
val pairedDevices = bluetoothManager.adapter.bondedDevices.toList()
val pairedAddresses = pairedDevices.map { it.address }.toSet()
// Scan devices
val scannedDevices = ArrayList<BluetoothPeripheral>()
central.scanForPeripherals({ peripheral, scanResult ->
if (peripheral.name.isBlank() || scannedDevices.contains(peripheral)) return@scanForPeripherals
// Add to scanned devices
scannedDevices.add(peripheral)
Handler(Looper.getMainLooper()).post {
binding.lvScanned.adapter = ArrayAdapter(applicationContext, android.R.layout.simple_list_item_1,
scannedDevices.map {
"${it.name + if (it.address in pairedAddresses) " (Paired)" else ""}\n" +
"MAC Address: ${it.address}"
})
}
}, {})
// Click scanned device
binding.lvScanned.setOnItemClickListener { parent, view, position, id ->
central.stopScan()
bluetoothHandler.connectPeripheral(scannedDevices[position])
} }
// Format and show bounded device list
binding.lvPaired.adapter = ArrayAdapter(applicationContext, android.R.layout.simple_list_item_1,
pairedDevices.map { "Name: ${it.name}\nMAC Address: ${it.address}" })
private fun initBluetoothHandler() { // On click handler
val bluetoothHandler = getInstance(applicationContext) binding.lvPaired.setOnItemClickListener { parent, view, position, id ->
// Extract MAC address
val dev = pairedDevices[position]
println("Clicked: ${dev.address}")
view.snack("Connecting...")
// Scan for the device with the MAC address
central.stopScan()
central.scanForPeripheralsWithAddresses(arrayOf(dev.address), { peripheral, scanResult ->
if (peripheral.address != dev.address) return@scanForPeripheralsWithAddresses
view.snack("✅ Connected.")
central.stopScan()
bluetoothHandler.connectPeripheral(peripheral)
}, {})
}
collectBloodPressure(bluetoothHandler)
collectHeartRate(bluetoothHandler) collectHeartRate(bluetoothHandler)
collectGlucose(bluetoothHandler)
collectPulseOxContinuous(bluetoothHandler) collectPulseOxContinuous(bluetoothHandler)
collectPulseOxSpot(bluetoothHandler) collectPulseOxSpot(bluetoothHandler)
collectTemperature(bluetoothHandler) collectTemperature(bluetoothHandler)
collectWeight(bluetoothHandler) collectWeight(bluetoothHandler)
} }
private fun collectBloodPressure(bluetoothHandler: BluetoothHandler) {
scope.launch {
bluetoothHandler.bloodpressureChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue!!.text = String.format(
Locale.ENGLISH,
"%.0f/%.0f %s, %.0f bpm\n%s\n",
it.systolic,
it.diastolic,
if (it.unit == ObservationUnit.MMHG) "mmHg" else "kpa",
it.pulseRate,
dateFormat.format(it.timestamp ?: Calendar.getInstance())
)
}
}
}
}
private fun collectGlucose(bluetoothHandler: BluetoothHandler) {
scope.launch {
bluetoothHandler.glucoseChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue!!.text = String.format(
Locale.ENGLISH,
"%.1f %s\n%s\n",
it.value,
if (it.unit === ObservationUnit.MmolPerLiter) "mmol/L" else "mg/dL",
dateFormat.format(it.timestamp ?: Calendar.getInstance()),
)
}
}
}
}
private fun collectHeartRate(bluetoothHandler: BluetoothHandler) { private fun collectHeartRate(bluetoothHandler: BluetoothHandler) {
scope.launch { scope.launch {
bluetoothHandler.heartRateChannel.consumeAsFlow().collect { bluetoothHandler.heartRateChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
measurementValue?.text = String.format(Locale.ENGLISH, "%d bpm", it.pulse) binding.mainText.text = String.format(Locale.ENGLISH, "%d bpm", it.pulse)
} }
} }
} }
@@ -141,7 +162,7 @@ class MainActivity : AppCompatActivity() {
scope.launch { scope.launch {
bluetoothHandler.pulseOxContinuousChannel.consumeAsFlow().collect { bluetoothHandler.pulseOxContinuousChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
measurementValue!!.text = String.format( binding.mainText.text = String.format(
Locale.ENGLISH, Locale.ENGLISH,
"SpO2 %d%%, Pulse %d bpm\n%s\n\nfrom %s", "SpO2 %d%%, Pulse %d bpm\n%s\n\nfrom %s",
it.spO2, it.spO2,
@@ -157,7 +178,7 @@ class MainActivity : AppCompatActivity() {
scope.launch { scope.launch {
bluetoothHandler.pulseOxSpotChannel.consumeAsFlow().collect { bluetoothHandler.pulseOxSpotChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
measurementValue!!.text = String.format( binding.mainText.text = String.format(
Locale.ENGLISH, Locale.ENGLISH,
"SpO2 %d%%, Pulse %d bpm\n", "SpO2 %d%%, Pulse %d bpm\n",
it.spO2, it.spO2,
@@ -172,7 +193,7 @@ class MainActivity : AppCompatActivity() {
scope.launch { scope.launch {
bluetoothHandler.temperatureChannel.consumeAsFlow().collect { bluetoothHandler.temperatureChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
measurementValue?.text = String.format( binding.mainText.text = String.format(
Locale.ENGLISH, Locale.ENGLISH,
"%.1f %s (%s)\n%s\n", "%.1f %s (%s)\n%s\n",
it.temperatureValue, it.temperatureValue,
@@ -189,7 +210,7 @@ class MainActivity : AppCompatActivity() {
scope.launch { scope.launch {
bluetoothHandler.weightChannel.consumeAsFlow().collect { bluetoothHandler.weightChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
measurementValue!!.text = String.format( binding.mainText.text = String.format(
Locale.ENGLISH, Locale.ENGLISH,
"%.1f %s\n%s\n", "%.1f %s\n%s\n",
it.weight, it.unit.toString(), it.weight, it.unit.toString(),
@@ -217,8 +238,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun getPeripheral(peripheralAddress: String): BluetoothPeripheral { private fun getPeripheral(peripheralAddress: String): BluetoothPeripheral {
val central = getInstance(applicationContext).central return bluetoothHandler.central.getPeripheral(peripheralAddress)
return central.getPeripheral(peripheralAddress)
} }
private fun checkPermissions() { private fun checkPermissions() {
@@ -277,7 +297,7 @@ class MainActivity : AppCompatActivity() {
private fun checkLocationServices(): Boolean { private fun checkLocationServices(): Boolean {
return if (!areLocationServicesEnabled()) { return if (!areLocationServicesEnabled()) {
AlertDialog.Builder(this@MainActivity) AlertDialog.Builder(this)
.setTitle("Location services are not enabled") .setTitle("Location services are not enabled")
.setMessage("Scanning for Bluetooth peripherals requires locations services to be enabled.") // Want to enable? .setMessage("Scanning for Bluetooth peripherals requires locations services to be enabled.") // Want to enable?
.setPositiveButton("Enable") { dialogInterface, _ -> .setPositiveButton("Enable") { dialogInterface, _ ->
@@ -315,7 +335,7 @@ class MainActivity : AppCompatActivity() {
if (allGranted) { if (allGranted) {
checkIfLocationIsNeeded() checkIfLocationIsNeeded()
} else { } else {
AlertDialog.Builder(this@MainActivity) AlertDialog.Builder(this)
.setTitle("Location permission is required for scanning Bluetooth peripherals") .setTitle("Location permission is required for scanning Bluetooth peripherals")
.setMessage("Please grant permissions") .setMessage("Please grant permissions")
.setPositiveButton("Retry") { dialogInterface, _ -> .setPositiveButton("Retry") { dialogInterface, _ ->
@@ -0,0 +1,7 @@
package org.hydev.wearsync
import android.view.View
import com.google.android.material.snackbar.Snackbar
fun View.snack(msg: String) = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
@@ -218,7 +218,7 @@ internal class BluetoothHandler private constructor(context: Context) {
{ scanFailure -> Timber.e("scan failed with reason $scanFailure") }) { scanFailure -> Timber.e("scan failed with reason $scanFailure") })
} }
private fun connectPeripheral(peripheral: BluetoothPeripheral) { fun connectPeripheral(peripheral: BluetoothPeripheral) {
peripheral.observeBondState { peripheral.observeBondState {
Timber.i("Bond state is $it") Timber.i("Bond state is $it")
} }
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/mainText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Waiting for measurement..."
android:textAlignment="center"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvPaired"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Paired Devices"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvScanned"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Scanned Devices"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lvPaired" />
<ListView
android:id="@+id/lvPaired"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvPaired" />
<ListView
android:id="@+id/lvScanned"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvScanned" />
</androidx.constraintlayout.widget.ConstraintLayout>