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

407 lines
14 KiB
Kotlin
Raw Normal View History

2020-02-13 09:25:39 -08:00
package com.geeksville.mesh.ui
2020-04-08 21:17:23 -07:00
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
2020-04-08 18:42:17 -07:00
import android.os.Bundle
2020-04-08 21:17:23 -07:00
import android.os.ParcelUuid
2020-04-08 18:42:17 -07:00
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
2020-04-08 21:17:23 -07:00
import android.widget.RadioButton
2020-04-08 18:42:17 -07:00
import androidx.fragment.app.activityViewModels
2020-04-08 21:17:23 -07:00
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
2020-04-08 18:42:17 -07:00
import com.geeksville.android.Logging
import com.geeksville.mesh.R
2020-04-08 21:17:23 -07:00
import com.geeksville.mesh.service.RadioInterfaceService
import com.geeksville.util.exceptionReporter
import kotlinx.android.synthetic.main.settings_fragment.*
2020-04-08 18:42:17 -07:00
2020-04-08 21:17:23 -07:00
class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
2020-04-08 18:42:17 -07:00
2020-04-08 21:17:23 -07:00
private val context = getApplication<Application>().applicationContext
2020-04-08 18:42:17 -07:00
2020-04-08 21:17:23 -07:00
init {
debug("BTScanModel created")
2020-04-08 18:42:17 -07:00
}
2020-02-18 08:56:53 -08:00
2020-04-08 21:17:23 -07:00
data class BTScanEntry(val name: String, val macAddress: String, val bonded: Boolean) {
// val isSelected get() = macAddress == selectedMacAddr
2020-02-18 08:56:53 -08:00
}
2020-04-08 21:17:23 -07:00
override fun onCleared() {
super.onCleared()
debug("BTScanModel cleared")
2020-02-18 08:56:53 -08:00
}
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-04-08 21:17:23 -07:00
var selectedMacAddr: String? = null
val errorText = object : MutableLiveData<String?>(null) {}
val devices = object : LiveData<Map<String, BTScanEntry>>(mapOf()) {
2020-02-13 09:25:39 -08:00
2020-04-08 21:17:23 -07:00
private var scanner: BluetoothLeScanner? = null
private val scanCallback = object : ScanCallback() {
2020-02-18 09:09:49 -08:00
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-04-08 21:17:23 -07:00
errorText.value = msg
2020-02-18 09:09:49 -08:00
}
// 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
val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED
2020-04-08 21:17:23 -07:00
val oldDevs = value!!
val oldEntry = oldDevs[addr]
if (oldEntry == null || oldEntry.bonded != isBonded) {
2020-02-18 09:09:49 -08:00
val entry = BTScanEntry(
result.device.name,
addr,
isBonded
2020-02-18 09:09:49 -08:00
)
2020-04-08 21:17:23 -07:00
debug("onScanResult ${entry}")
2020-02-18 09:09:49 -08:00
// If nothing was selected, by default select the first thing we see
2020-04-08 21:17:23 -07:00
if (selectedMacAddr == null && entry.bonded)
changeSelection(context, addr)
value = oldDevs + Pair(addr, entry) // trigger gui updates
2020-02-18 09:09:49 -08:00
}
}
}
2020-02-13 19:02:40 -08:00
2020-04-08 21:17:23 -07:00
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
}
}
2020-02-13 19:02:40 -08:00
2020-04-08 21:17:23 -07:00
private fun startScan() {
debug("BTScan component active")
selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
2020-02-13 19:02:40 -08:00
2020-04-08 21:17:23 -07:00
if (bluetoothAdapter == null) {
warn("No bluetooth adapter. Running under emulation?")
val testnodes = listOf(
BTScanEntry("Meshtastic_ab12", "xx", false),
BTScanEntry("Meshtastic_32ac", "xb", true)
)
2020-04-08 21:17:23 -07:00
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 {
2020-04-08 21:17:23 -07:00
/// 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)
2020-04-08 21:34:57 -07:00
scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg ->
if (errMsg != null) {
scanStatusText.text = errMsg
}
})
2020-04-08 21:17:23 -07:00
scanModel.devices.observe(viewLifecycleOwner, Observer { devices ->
// Remove the old radio buttons and repopulate
deviceRadioGroup.removeAllViews()
2020-04-08 21:34:57 -07:00
var hasBonded = false // Have any of our devices been bonded
2020-04-08 21:17:23 -07:00
devices.values.forEach { device ->
2020-04-08 21:34:57 -07:00
hasBonded = hasBonded || device.bonded
2020-04-08 21:17:23 -07:00
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)
}
}
2020-04-08 21:34:57 -07:00
// get rid of the warning text once at least one device is paired
warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE
2020-04-08 21:17:23 -07:00
})
}
}
/*
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
)
2020-02-18 08:56:53 -08:00
}
2020-02-13 09:25:39 -08:00
}
2020-04-08 21:17:23 -07:00
}
*/
/*
@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
}
2020-02-13 09:25:39 -08:00
Column {
2020-02-18 08:56:53 -08:00
if (ScanUIState.errorText != null) {
2020-04-05 18:00:59 -07:00
Text(text = 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()
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
ProvideEmphasis(emphasis = if (it.bonded) MaterialTheme.emphasisLevels.high else MaterialTheme.emphasisLevels.disabled) {
2020-02-13 19:54:05 -08:00
RadioGroupTextItem(
selected = (it.isSelected),
onSelect = {
2020-04-08 21:17:23 -07:00
2020-02-13 19:54:05 -08:00
},
text = it.name
)
}
2020-02-13 19:02:40 -08:00
}
}
}
}
}
2020-02-13 09:25:39 -08:00
}
}
*/