sforkowany z mirror/meshtastic-android
bt scan kinda works
rodzic
c286c56067
commit
fb06046796
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
|
@ -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>
|
Ładowanie…
Reference in New Issue