[+] Scan activity
This commit is contained in:
+88
-68
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user