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-09 11:03:17 -07:00
|
|
|
import android.view.inputmethod.EditorInfo
|
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.MutableLiveData
|
|
|
|
|
import androidx.lifecycle.Observer
|
2020-04-09 13:28:44 -07:00
|
|
|
import com.geeksville.android.GeeksvilleApplication
|
2020-04-08 18:42:17 -07:00
|
|
|
import com.geeksville.android.Logging
|
2020-04-09 11:03:17 -07:00
|
|
|
import com.geeksville.android.hideKeyboard
|
2020-04-09 13:28:44 -07:00
|
|
|
import com.geeksville.mesh.MainActivity
|
2020-04-08 18:42:17 -07:00
|
|
|
import com.geeksville.mesh.R
|
2020-04-09 11:03:17 -07:00
|
|
|
import com.geeksville.mesh.model.UIViewModel
|
2020-04-08 21:17:23 -07:00
|
|
|
import com.geeksville.mesh.service.RadioInterfaceService
|
|
|
|
|
import com.geeksville.util.exceptionReporter
|
2020-04-15 14:10:40 -07:00
|
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
2020-04-08 21:17:23 -07:00
|
|
|
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) {}
|
|
|
|
|
|
2020-02-13 09:25:39 -08:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
private var scanner: BluetoothLeScanner? = null
|
2020-04-08 21:17:23 -07:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
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
|
|
|
|
|
errorText.value = msg
|
|
|
|
|
}
|
2020-02-18 09:09:49 -08:00
|
|
|
|
2020-04-09 11:03:17 -07: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
|
|
|
|
|
val oldDevs = devices.value!!
|
|
|
|
|
val oldEntry = oldDevs[addr]
|
|
|
|
|
if (oldEntry == null || oldEntry.bonded != isBonded) {
|
|
|
|
|
val entry = BTScanEntry(
|
2020-04-12 08:58:09 -07:00
|
|
|
result.device.name
|
|
|
|
|
?: "unnamed-$addr", // autobug: some devices might not have a name, if someone is running really old device code?
|
2020-04-09 11:03:17 -07:00
|
|
|
addr,
|
|
|
|
|
isBonded
|
|
|
|
|
)
|
|
|
|
|
debug("onScanResult ${entry}")
|
|
|
|
|
|
|
|
|
|
// If nothing was selected, by default select the first thing we see
|
|
|
|
|
if (selectedMacAddr == null && entry.bonded)
|
2020-04-09 13:28:44 -07:00
|
|
|
changeSelection(GeeksvilleApplication.currentActivity as MainActivity, addr)
|
2020-04-09 11:03:17 -07:00
|
|
|
|
|
|
|
|
devices.value = oldDevs + Pair(addr, entry) // trigger gui updates
|
2020-02-18 09:09:49 -08:00
|
|
|
}
|
|
|
|
|
}
|
2020-04-09 11:03:17 -07:00
|
|
|
}
|
2020-02-13 19:02:40 -08:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
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}")
|
2020-04-08 21:17:23 -07:00
|
|
|
}
|
2020-04-09 11:03:17 -07:00
|
|
|
scanner = null
|
2020-04-08 21:17:23 -07:00
|
|
|
}
|
2020-04-09 11:03:17 -07:00
|
|
|
}
|
2020-02-13 19:02:40 -08:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
fun startScan() {
|
|
|
|
|
debug("BTScan component active")
|
|
|
|
|
selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
|
2020-02-13 19:02:40 -08:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
if (bluetoothAdapter == null) {
|
|
|
|
|
warn("No bluetooth adapter. Running under emulation?")
|
2020-04-08 21:17:23 -07:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
val testnodes = listOf(
|
|
|
|
|
BTScanEntry("Meshtastic_ab12", "xx", false),
|
|
|
|
|
BTScanEntry("Meshtastic_32ac", "xb", true)
|
|
|
|
|
)
|
2020-02-29 14:14:52 -08:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
devices.value = (testnodes.map { it.macAddress to it }).toMap()
|
2020-04-08 21:17:23 -07:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
// If nothing was selected, by default select the first thing we see
|
|
|
|
|
if (selectedMacAddr == null)
|
2020-04-09 13:28:44 -07:00
|
|
|
changeSelection(
|
|
|
|
|
GeeksvilleApplication.currentActivity as MainActivity,
|
|
|
|
|
testnodes.first().macAddress
|
|
|
|
|
)
|
2020-04-09 11:03:17 -07: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) {
|
|
|
|
|
errorText.value =
|
2020-04-09 11:27:42 -07:00
|
|
|
context.getString(R.string.requires_bluetooth)
|
2020-03-03 20:07:19 -08:00
|
|
|
} else {
|
2020-04-09 11:03:17 -07:00
|
|
|
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
|
2020-04-08 21:17:23 -07:00
|
|
|
}
|
|
|
|
|
}
|
2020-04-09 11:03:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val devices = object : MutableLiveData<Map<String, BTScanEntry>>(mapOf()) {
|
|
|
|
|
|
2020-04-08 21:17:23 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 by the GUI when a new device has been selected by the user
|
|
|
|
|
/// Returns true if we were able to change to that item
|
2020-04-15 13:21:29 -07:00
|
|
|
fun onSelected(activity: MainActivity, it: BTScanEntry): Boolean {
|
2020-04-08 21:17:23 -07:00
|
|
|
// If the device is paired, let user select it, otherwise start the pairing flow
|
|
|
|
|
if (it.bonded) {
|
2020-04-15 13:21:29 -07:00
|
|
|
changeSelection(activity, it.macAddress)
|
2020-04-08 21:17:23 -07:00
|
|
|
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(
|
2020-04-15 13:21:29 -07:00
|
|
|
activity,
|
2020-04-08 21:17:23 -07:00
|
|
|
it.macAddress
|
|
|
|
|
)
|
2020-04-09 12:22:41 -07:00
|
|
|
|
|
|
|
|
// Force the GUI to redraw
|
|
|
|
|
devices.value = devices.value
|
2020-04-08 21:17:23 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2020-04-09 13:28:44 -07:00
|
|
|
fun changeSelection(context: MainActivity, newAddr: String) {
|
2020-04-08 21:17:23 -07:00
|
|
|
info("Changing BT device to $newAddr")
|
|
|
|
|
selectedMacAddr = newAddr
|
|
|
|
|
RadioInterfaceService.setBondedDeviceAddress(context, newAddr)
|
2020-04-09 13:28:44 -07:00
|
|
|
|
|
|
|
|
// Super ugly hack. we force the activity to reconnect FIXME, find a cleaner way
|
|
|
|
|
context.unbindMeshService()
|
|
|
|
|
context.bindMeshService()
|
2020-04-08 21:17:23 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|
|
|
|
|
|
|
|
|
private val scanModel: BTScanModel by activityViewModels()
|
2020-04-09 11:03:17 -07:00
|
|
|
private val model: UIViewModel by activityViewModels()
|
2020-04-08 21:17:23 -07:00
|
|
|
|
|
|
|
|
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-09 11:03:17 -07:00
|
|
|
model.ownerName.observe(viewLifecycleOwner, Observer { name ->
|
|
|
|
|
usernameEditText.setText(name)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
usernameEditText.on(EditorInfo.IME_ACTION_DONE) {
|
|
|
|
|
debug("did IME action")
|
|
|
|
|
val n = usernameEditText.text.toString().trim()
|
|
|
|
|
if (n.isNotEmpty())
|
2020-04-09 16:33:42 -07:00
|
|
|
model.setOwner(n)
|
2020-04-09 11:03:17 -07:00
|
|
|
|
|
|
|
|
requireActivity().hideKeyboard()
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-15 14:10:40 -07:00
|
|
|
val app = (requireContext().applicationContext as GeeksvilleApplication)
|
|
|
|
|
|
2020-04-11 13:20:30 -07:00
|
|
|
// Set analytics checkbox
|
2020-04-15 14:10:40 -07:00
|
|
|
analyticsOkayCheckbox.isChecked = app.isAnalyticsAllowed
|
|
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
analyticsOkayCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
2020-04-11 13:20:30 -07:00
|
|
|
debug("User changed analytics to $isChecked")
|
2020-04-15 14:10:40 -07:00
|
|
|
app.isAnalyticsAllowed = isChecked
|
|
|
|
|
reportBugButton.isEnabled = app.isAnalyticsAllowed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// report bug button only enabled if analytics is allowed
|
|
|
|
|
reportBugButton.isEnabled = app.isAnalyticsAllowed
|
|
|
|
|
reportBugButton.setOnClickListener {
|
|
|
|
|
MaterialAlertDialogBuilder(requireContext())
|
|
|
|
|
.setTitle(R.string.report_a_bug)
|
|
|
|
|
.setMessage(getString(R.string.report_bug_text))
|
|
|
|
|
.setNeutralButton(R.string.cancel) { _, _ ->
|
|
|
|
|
debug("Decided not to report a bug")
|
|
|
|
|
}
|
|
|
|
|
.setPositiveButton(getString(R.string.report)) { _, _ ->
|
|
|
|
|
reportError("Clicked Report A Bug")
|
|
|
|
|
}
|
|
|
|
|
.show()
|
|
|
|
|
|
|
|
|
|
true
|
2020-04-09 11:03:17 -07:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2020-04-08 21:17:23 -07:00
|
|
|
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 {
|
2020-04-09 13:28:44 -07:00
|
|
|
scanProgressBar.visibility = View.INVISIBLE
|
|
|
|
|
if (!device.bonded)
|
|
|
|
|
scanStatusText.setText(R.string.starting_pairing)
|
|
|
|
|
|
2020-04-15 13:21:29 -07:00
|
|
|
b.isSelected = scanModel.onSelected(requireActivity() as MainActivity, device)
|
2020-04-09 13:28:44 -07:00
|
|
|
|
|
|
|
|
if (!b.isSelected)
|
|
|
|
|
scanStatusText.setText(R.string.pairing_failed)
|
2020-04-08 21:17:23 -07:00
|
|
|
}
|
2020-02-29 14:14:52 -08:00
|
|
|
}
|
2020-04-08 21:34:57 -07:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
val hasBonded = RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null
|
|
|
|
|
|
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
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
override fun onPause() {
|
|
|
|
|
super.onPause()
|
|
|
|
|
scanModel.stopScan()
|
2020-04-08 21:17:23 -07:00
|
|
|
}
|
2020-02-13 09:25:39 -08:00
|
|
|
|
2020-04-09 11:03:17 -07:00
|
|
|
override fun onResume() {
|
|
|
|
|
super.onResume()
|
|
|
|
|
scanModel.startScan()
|
2020-02-13 09:25:39 -08:00
|
|
|
}
|
|
|
|
|
}
|
2020-04-09 11:03:17 -07:00
|
|
|
|