bt scan kinda works

1.2-legacy
geeksville 2020-04-08 21:17:23 -07:00
rodzic c286c56067
commit fb06046796
3 zmienionych plików z 309 dodań i 219 usunięć

Wyświetl plik

@ -1,102 +1,64 @@
package com.geeksville.mesh.ui
import android.app.Application
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.os.ParcelUuid
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.geeksville.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.RadioInterfaceService
import com.geeksville.util.exceptionReporter
import kotlinx.android.synthetic.main.settings_fragment.*
class SettingsFragment : ScreenFragment("Settings"), Logging {
class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
private val model: UIViewModel by activityViewModels()
private val context = getApplication<Application>().applicationContext
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.settings_fragment, container, false)
init {
debug("BTScanModel created")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
data class BTScanEntry(val name: String, val macAddress: String, val bonded: Boolean) {
// val isSelected get() = macAddress == selectedMacAddr
}
}
/*
@Model
object ScanUIState {
var selectedMacAddr: String? = null
var errorText: String? = null
val devices = modelMapOf<String, BTScanEntry>()
/// Change to a new macaddr selection, updating GUI and radio
fun changeSelection(context: Context, newAddr: String) {
ScanState.info("Changing BT device to $newAddr")
selectedMacAddr = newAddr
RadioInterfaceService.setBondedDeviceAddress(context, newAddr)
override fun onCleared() {
super.onCleared()
debug("BTScanModel cleared")
}
}
/// 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}")
}
callback = null
}
}
}
@Model
data class BTScanEntry(val name: String, val macAddress: String, val bonded: Boolean) {
val isSelected get() = macAddress == ScanUIState.selectedMacAddr
}
class BTScanFragment(screenName: String, id: Int, private val content: @Composable() () -> Unit) :
ComposeFragment(screenName, id, content) {
override fun onStop() {
ScanState.stopScan()
super.onStop()
}
}
@Composable
fun BTScanScreen() {
val context = ContextAmbient.current
/// Note: may be null on platforms without a bluetooth driver (ie. the emulator)
val bluetoothAdapter =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
// FIXME - remove onCommit now that we have a fragement to run in
onCommit() {
ScanState.debug("BTScan component active")
ScanUIState.selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
var selectedMacAddr: String? = null
val errorText = object : MutableLiveData<String?>(null) {}
val scanCallback = object : ScanCallback() {
val devices = object : LiveData<Map<String, BTScanEntry>>(mapOf()) {
private var scanner: BluetoothLeScanner? = null
private val scanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
val msg = "Unexpected bluetooth scan failure: $errorCode"
// error code2 seeems to be indicate hung bluetooth stack
ScanUIState.errorText = msg
ScanState.reportError(msg)
errorText.value = msg
}
// For each device that appears in our scan, ask for its GATT, when the gatt arrives,
@ -107,58 +69,292 @@ fun BTScanScreen() {
val addr = result.device.address
// prevent logspam because weill get get lots of redundant scan results
val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED
val oldEntry = ScanUIState.devices[addr]
val oldDevs = value!!
val oldEntry = oldDevs[addr]
if (oldEntry == null || oldEntry.bonded != isBonded) {
val entry = BTScanEntry(
result.device.name,
addr,
isBonded
)
ScanState.debug("onScanResult ${entry}")
ScanUIState.devices[addr] = entry
debug("onScanResult ${entry}")
// If nothing was selected, by default select the first thing we see
if (ScanUIState.selectedMacAddr == null && entry.bonded)
ScanUIState.changeSelection(context, addr)
if (selectedMacAddr == null && entry.bonded)
changeSelection(context, addr)
value = oldDevs + Pair(addr, entry) // trigger gui updates
}
}
}
if (bluetoothAdapter == null) {
ScanState.warn("No bluetooth adapter. Running under emulation?")
val testnodes = listOf(
BTScanEntry("Meshtastic_ab12", "xx", false),
BTScanEntry("Meshtastic_32ac", "xb", true)
)
ScanUIState.devices.putAll(testnodes.map { it.macAddress to it })
// If nothing was selected, by default select the first thing we see
if (ScanUIState.selectedMacAddr == null)
ScanUIState.changeSelection(context, testnodes.first().macAddress)
} 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
private fun stopScan() {
if (scanner != null) {
debug("stopping scan")
try {
scanner?.stopScan(scanCallback)
} catch (ex: Throwable) {
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
}
scanner = null
}
}
private fun startScan() {
debug("BTScan component active")
selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
if (bluetoothAdapter == null) {
warn("No bluetooth adapter. Running under emulation?")
val testnodes = listOf(
BTScanEntry("Meshtastic_ab12", "xx", false),
BTScanEntry("Meshtastic_32ac", "xb", true)
)
value = (testnodes.map { it.macAddress to it }).toMap()
// If nothing was selected, by default select the first thing we see
if (selectedMacAddr == null)
changeSelection(context, testnodes.first().macAddress)
} else {
/// The following call might return null if the user doesn't have bluetooth access permissions
val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner
if (s == null) {
errorText.value =
"This application requires bluetooth access. Please grant access in android settings."
} else {
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)
scanner = s
}
}
}
/**
* Called when the number of active observers change from 1 to 0.
*
*
* This does not mean that there are no observers left, there may still be observers but their
* lifecycle states aren't [Lifecycle.State.STARTED] or [Lifecycle.State.RESUMED]
* (like an Activity in the back stack).
*
*
* You can check if there are observers via [.hasObservers].
*/
override fun onInactive() {
super.onInactive()
stopScan()
}
/**
* Called when the number of active observers change to 1 from 0.
*
*
* This callback can be used to know that this LiveData is being used thus should be kept
* up to date.
*/
override fun onActive() {
super.onActive()
startScan()
}
}
/// Called by the GUI when a new device has been selected by the user
/// Returns true if we were able to change to that item
fun onSelected(it: BTScanEntry): Boolean {
// If the device is paired, let user select it, otherwise start the pairing flow
if (it.bonded) {
changeSelection(context, it.macAddress)
return true
} else {
info("Starting bonding for $it")
// We need this receiver to get informed when the bond attempt finished
val bondChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent
) = exceptionReporter {
val state =
intent.getIntExtra(
BluetoothDevice.EXTRA_BOND_STATE,
-1
)
debug("Received bond state changed $state")
context.unregisterReceiver(this)
if (state == BluetoothDevice.BOND_BONDED || state == BluetoothDevice.BOND_BONDING) {
debug("Bonding completed, connecting service")
changeSelection(
context,
it.macAddress
)
}
}
}
val filter = IntentFilter()
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
context.registerReceiver(bondChangedReceiver, filter)
// We ignore missing BT adapters, because it lets us run on the emulator
bluetoothAdapter
?.getRemoteDevice(it.macAddress)
?.createBond()
return false
}
}
/// Change to a new macaddr selection, updating GUI and radio
fun changeSelection(context: Context, newAddr: String) {
info("Changing BT device to $newAddr")
selectedMacAddr = newAddr
RadioInterfaceService.setBondedDeviceAddress(context, newAddr)
}
}
class SettingsFragment : ScreenFragment("Settings"), Logging {
private val scanModel: BTScanModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.settings_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
scanModel.devices.observe(viewLifecycleOwner, Observer { devices ->
// Remove the old radio buttons and repopulate
deviceRadioGroup.removeAllViews()
devices.values.forEach { device ->
val b = RadioButton(requireActivity())
b.text = device.name
b.id = View.generateViewId()
b.isEnabled =
true // Now we always want to enable, if the user clicks we'll try to bond device.bonded
b.isSelected = device.macAddress == scanModel.selectedMacAddr
deviceRadioGroup.addView(b)
b.setOnClickListener {
b.isChecked = scanModel.onSelected(device)
}
}
})
}
}
/*
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.core.ContextAmbient
import androidx.ui.foundation.Text
import androidx.ui.input.ImeAction
import androidx.ui.layout.*
import androidx.ui.material.MaterialTheme
import androidx.ui.text.TextStyle
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.android.Logging
import com.geeksville.mesh.model.MessagesState
import com.geeksville.mesh.model.UIState
import com.geeksville.mesh.service.RadioInterfaceService
object SettingsLog : Logging
@Composable
fun SettingsContent() {
//val typography = MaterialTheme.typography()
val context = ContextAmbient.current
Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) {
Row {
Text("Your name ", modifier = LayoutGravity.Center)
val name = state { UIState.ownerName }
StyledTextField(
value = name.value,
onValueChange = { name.value = it },
textStyle = TextStyle(
color = palette.onSecondary.copy(alpha = 0.8f)
),
imeAction = ImeAction.Done,
onImeActionPerformed = {
MessagesState.info("did IME action")
val n = name.value.trim()
if (n.isNotEmpty())
UIState.setOwner(context, n)
},
hintText = "Type your name here...",
modifier = LayoutGravity.Center
)
}
BTScanScreen()
val bonded = RadioInterfaceService.getBondedDeviceAddress(context) != null
if (!bonded) {
val typography = MaterialTheme.typography
Text(
text =
"""
You haven't yet paired a Meshtastic compatible radio with this phone.
This application is an early alpha release, if you find problems please post on our website chat.
For more information see our web page - www.meshtastic.org.
""".trimIndent(), style = typography.body2
)
}
}
}
*/
/*
@Model
object ScanUIState {
}
/// FIXME, remove once compose has better lifecycle management
object ScanState : Logging {
}
@Composable
fun BTScanScreen() {
val context = ContextAmbient.current
// FIXME - remove onCommit now that we have a fragement to run in
}
Column {
@ -183,45 +379,7 @@ fun BTScanScreen() {
RadioGroupTextItem(
selected = (it.isSelected),
onSelect = {
// If the device is paired, let user select it, otherwise start the pairing flow
if (it.bonded) {
ScanUIState.changeSelection(context, it.macAddress)
} else {
ScanState.info("Starting bonding for $it")
// We need this receiver to get informed when the bond attempt finished
val bondChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent
) = exceptionReporter {
val state =
intent.getIntExtra(
BluetoothDevice.EXTRA_BOND_STATE,
-1
)
ScanState.debug("Received bond state changed $state")
context.unregisterReceiver(this)
if (state == BluetoothDevice.BOND_BONDED || state == BluetoothDevice.BOND_BONDING) {
ScanState.debug("Bonding completed, connecting service")
ScanUIState.changeSelection(
context,
it.macAddress
)
}
}
}
val filter = IntentFilter()
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
context.registerReceiver(bondChangedReceiver, filter)
// We ignore missing BT adapters, because it lets us run on the emulator
bluetoothAdapter
?.getRemoteDevice(it.macAddress)
?.createBond()
}
},
text = it.name
)

Wyświetl plik

@ -1,72 +1,2 @@
package com.geeksville.mesh.ui
/*
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.core.ContextAmbient
import androidx.ui.foundation.Text
import androidx.ui.input.ImeAction
import androidx.ui.layout.*
import androidx.ui.material.MaterialTheme
import androidx.ui.text.TextStyle
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.android.Logging
import com.geeksville.mesh.model.MessagesState
import com.geeksville.mesh.model.UIState
import com.geeksville.mesh.service.RadioInterfaceService
object SettingsLog : Logging
@Composable
fun SettingsContent() {
//val typography = MaterialTheme.typography()
val context = ContextAmbient.current
Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) {
Row {
Text("Your name ", modifier = LayoutGravity.Center)
val name = state { UIState.ownerName }
StyledTextField(
value = name.value,
onValueChange = { name.value = it },
textStyle = TextStyle(
color = palette.onSecondary.copy(alpha = 0.8f)
),
imeAction = ImeAction.Done,
onImeActionPerformed = {
MessagesState.info("did IME action")
val n = name.value.trim()
if (n.isNotEmpty())
UIState.setOwner(context, n)
},
hintText = "Type your name here...",
modifier = LayoutGravity.Center
)
}
BTScanScreen()
val bonded = RadioInterfaceService.getBondedDeviceAddress(context) != null
if (!bonded) {
val typography = MaterialTheme.typography
Text(
text =
"""
You haven't yet paired a Meshtastic compatible radio with this phone.
This application is an early alpha release, if you find problems please post on our website chat.
For more information see our web page - www.meshtastic.org.
""".trimIndent(), style = typography.body2
)
}
}
}
*/

Wyświetl plik

@ -87,7 +87,9 @@
android:text="@string/analytics_okay"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deviceRadioGroup"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>