Meshtastic-Android/app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt

200 lines
7.9 KiB
Kotlin
Raw Normal View History

2020-02-13 09:25:39 -08:00
package com.geeksville.mesh.ui
2020-02-13 19:54:05 -08:00
import android.bluetooth.BluetoothDevice
2020-02-13 09:25:39 -08:00
import android.bluetooth.BluetoothManager
2020-02-18 08:56:53 -08:00
import android.bluetooth.le.*
2020-02-13 09:25:39 -08:00
import android.os.ParcelUuid
2020-02-13 19:02:40 -08:00
import androidx.compose.*
import androidx.compose.frames.modelMapOf
2020-02-13 09:25:39 -08:00
import androidx.ui.core.ContextAmbient
import androidx.ui.core.LayoutModifier
2020-02-13 09:25:39 -08:00
import androidx.ui.core.Text
import androidx.ui.layout.Column
import androidx.ui.layout.LayoutGravity
2020-02-13 19:02:40 -08:00
import androidx.ui.material.CircularProgressIndicator
2020-02-13 19:54:05 -08:00
import androidx.ui.material.EmphasisLevels
import androidx.ui.material.ProvideEmphasis
2020-02-13 19:02:40 -08:00
import androidx.ui.material.RadioGroup
2020-02-13 09:25:39 -08:00
import androidx.ui.tooling.preview.Preview
import com.geeksville.android.Logging
import com.geeksville.mesh.service.RadioInterfaceService
2020-02-13 19:02:40 -08:00
@Model
2020-02-18 08:56:53 -08:00
object ScanUIState {
2020-02-13 19:02:40 -08:00
var selectedMacAddr: String? = null
var errorText: String? = null
2020-02-18 08:56:53 -08:00
val devices = modelMapOf<String, BTScanEntry>()
/// Change to a new macaddr selection, updating GUI and radio
2020-02-18 09:09:49 -08:00
fun changeSelection(context: Context, newAddr: String) {
2020-02-18 08:56:53 -08:00
ScanState.info("Changing BT device to $newAddr")
selectedMacAddr = newAddr
RadioInterfaceService.setBondedDeviceAddress(context, newAddr)
}
}
/// FIXME, remove once compose has better lifecycle management
object ScanState : Logging {
var scanner: BluetoothLeScanner? = null
var callback: ScanCallback? = null // SUPER NASTY FIXME
fun stopScan() {
if (callback != null) {
debug("stopping scan")
try {
scanner!!.stopScan(callback)
} catch(ex: Throwable) {
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
}
2020-02-18 10:40:02 -08:00
callback = null
2020-02-25 09:28:47 -08:00
}
2020-02-18 08:56:53 -08:00
}
2020-02-13 19:02:40 -08:00
}
@Model
2020-02-13 19:54:05 -08:00
data class BTScanEntry(val name: String, val macAddress: String, val bonded: Boolean) {
2020-02-18 08:56:53 -08:00
val isSelected get() = macAddress == ScanUIState.selectedMacAddr
2020-02-13 19:02:40 -08:00
}
2020-02-13 09:25:39 -08:00
@Composable
fun BTScanScreen() {
val context = ContextAmbient.current
2020-02-13 09:25:39 -08:00
/// Note: may be null on platforms without a bluetooth driver (ie. the emulator)
val bluetoothAdapter =
2020-02-18 10:40:02 -08:00
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
2020-02-13 19:02:40 -08:00
2020-02-13 09:25:39 -08:00
onActive {
2020-02-18 08:56:53 -08:00
ScanUIState.selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
2020-02-13 09:25:39 -08:00
2020-02-18 09:09:49 -08:00
val scanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
val msg = "Unexpected bluetooth scan failure: $errorCode"
2020-02-18 10:40:02 -08:00
// error code2 seeems to be indicate hung bluetooth stack
2020-02-18 09:09:49 -08:00
ScanUIState.errorText = msg
ScanState.reportError(msg)
}
// For each device that appears in our scan, ask for its GATT, when the gatt arrives,
// check if it is an eligable device and store it in our list of candidates
// if that device later disconnects remove it as a candidate
override fun onScanResult(callbackType: Int, result: ScanResult) {
val addr = result.device.address
// prevent logspam because weill get get lots of redundant scan results
if (!ScanUIState.devices.contains(addr)) {
val entry = BTScanEntry(
result.device.name,
addr,
result.device.bondState == BluetoothDevice.BOND_BONDED
)
ScanState.debug("onScanResult ${entry}")
ScanUIState.devices[addr] = entry
// If nothing was selected, by default select the first thing we see
if (ScanUIState.selectedMacAddr == null && entry.bonded)
ScanUIState.changeSelection(context, addr)
}
}
}
2020-02-13 19:02:40 -08:00
if (bluetoothAdapter == null) {
2020-02-18 08:56:53 -08:00
ScanState.warn("No bluetooth adapter. Running under emulation?")
2020-02-13 19:02:40 -08:00
val testnodes = listOf(
2020-02-13 19:54:05 -08:00
BTScanEntry("Meshtastic_ab12", "xx", false),
BTScanEntry("Meshtastic_32ac", "xb", true)
2020-02-13 19:02:40 -08:00
)
2020-02-18 08:56:53 -08:00
ScanUIState.devices.putAll(testnodes.map { it.macAddress to it })
2020-02-13 19:02:40 -08:00
// If nothing was selected, by default select the first thing we see
2020-02-18 08:56:53 -08:00
if (ScanUIState.selectedMacAddr == null)
2020-02-18 09:09:49 -08:00
ScanUIState.changeSelection(context, testnodes.first().macAddress)
2020-02-13 19:02:40 -08:00
} else {
/// The following call might return null if the user doesn't have bluetooth access permissions
val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner
if(s == null) {
ScanUIState.errorText = "This application requires bluetooth access. Please grant access in android settings."
}
else {
ScanState.debug("starting scan")
// filter and only accept devices that have a sw update service
val filter =
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID))
.build()
val settings =
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
s.startScan(listOf(filter), settings, scanCallback)
ScanState.scanner = s
ScanState.callback = scanCallback
}
2020-02-18 08:56:53 -08:00
}
2020-02-13 09:25:39 -08:00
2020-02-18 08:56:53 -08:00
onDispose {
ScanState.stopScan()
2020-02-13 09:25:39 -08:00
}
}
Column {
2020-02-18 08:56:53 -08:00
if (ScanUIState.errorText != null) {
Text("An unexpected error was encountered. Please file a bug on our github: ${ScanUIState.errorText}")
2020-02-13 19:02:40 -08:00
} else {
2020-02-18 08:56:53 -08:00
if (ScanUIState.devices.isEmpty()) {
Text(text = "Looking for Meshtastic devices... (zero found)", modifier = LayoutGravity.Center)
2020-02-13 19:02:40 -08:00
2020-02-13 19:54:05 -08:00
CircularProgressIndicator() // Show that we are searching still
} else {
// val allPaired = bluetoothAdapter?.bondedDevices.orEmpty().map { it.address }.toSet()
/* Only let user select paired devices
2020-02-13 19:02:40 -08:00
val paired = devices.values.filter { allPaired.contains(it.macAddress) }
if (paired.size < devices.size) {
Text(
"Warning: there are nearby Meshtastic devices that are not paired with this phone. Before you can select a device, you will need to pair it in Bluetooth Settings."
)
2020-02-13 19:54:05 -08:00
} */
2020-02-13 19:02:40 -08:00
RadioGroup {
Column {
2020-02-18 08:56:53 -08:00
ScanUIState.devices.values.forEach {
2020-02-13 19:02:40 -08:00
// disabled pending https://issuetracker.google.com/issues/149528535
2020-02-13 19:54:05 -08:00
ProvideEmphasis(emphasis = if (it.bonded) EmphasisLevels().high else EmphasisLevels().disabled) {
RadioGroupTextItem(
selected = (it.isSelected),
onSelect = {
// If the device is paired, let user select it, otherwise start the pairing flow
2020-02-18 09:09:49 -08:00
if (it.bonded) {
ScanUIState.changeSelection(context, it.macAddress)
} else {
2020-02-18 08:56:53 -08:00
ScanState.info("Starting bonding for $it")
2020-02-13 19:54:05 -08:00
// We ignore missing BT adapters, because it lets us run on the emulator
2020-02-18 08:56:53 -08:00
bluetoothAdapter
?.getRemoteDevice(it.macAddress)
2020-02-13 19:54:05 -08:00
?.createBond()
}
},
text = it.name
)
}
2020-02-13 19:02:40 -08:00
}
}
}
}
}
2020-02-13 09:25:39 -08:00
}
}
@Preview
@Composable
fun btScanScreenPreview() {
BTScanScreen()
}