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

965 lines
38 KiB
Kotlin
Raw Normal View History

2020-02-13 09:25:39 -08:00
package com.geeksville.mesh.ui
import android.annotation.SuppressLint
2020-04-08 21:17:23 -07:00
import android.app.Application
2020-06-08 14:04:56 -07:00
import android.app.PendingIntent
2020-04-08 21:17:23 -07:00
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.BOND_BONDED
import android.bluetooth.BluetoothDevice.BOND_BONDING
2020-04-08 21:17:23 -07:00
import android.bluetooth.le.*
import android.companion.AssociationRequest
import android.companion.BluetoothDeviceFilter
import android.companion.CompanionDeviceManager
import android.content.*
2020-06-08 14:04:56 -07:00
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
2020-04-08 18:42:17 -07:00
import android.os.Bundle
import android.os.RemoteException
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
2021-03-04 09:08:29 +08:00
import android.widget.AdapterView
2021-03-03 13:51:33 +08:00
import android.widget.ArrayAdapter
2020-04-08 21:17:23 -07:00
import android.widget.RadioButton
import android.widget.Toast
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
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-07-15 15:58:53 -07:00
import com.geeksville.android.isGooglePlayAvailable
import com.geeksville.mesh.MainActivity
2020-04-08 18:42:17 -07:00
import com.geeksville.mesh.R
import com.geeksville.mesh.android.bluetoothManager
import com.geeksville.mesh.android.usbManager
import com.geeksville.mesh.databinding.SettingsFragmentBinding
2020-04-09 11:03:17 -07:00
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.BluetoothInterface
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.RadioInterfaceService
2020-06-08 14:04:56 -07:00
import com.geeksville.mesh.service.SerialInterface
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ACTION_UPDATE_PROGRESS
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted
import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressSuccess
2020-05-30 14:38:16 -07:00
import com.geeksville.util.anonymize
2020-04-08 21:17:23 -07:00
import com.geeksville.util.exceptionReporter
import com.geeksville.util.exceptionToSnackbar
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
2020-04-15 14:10:40 -07:00
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hoho.android.usbserial.driver.UsbSerialDriver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.util.regex.Pattern
2020-04-08 18:42:17 -07:00
object SLogging : Logging {}
/// Change to a new macaddr selection, updating GUI and radio
fun changeDeviceSelection(context: MainActivity, newAddr: String?) {
// FIXME, this is a kinda yucky way to find the service
context.model.meshService?.let { service ->
MeshService.changeDeviceAddress(context, service, newAddr)
}
}
/// Show the UI asking the user to bond with a device, call changeSelection() if/when bonding completes
private fun requestBonding(
activity: MainActivity,
device: BluetoothDevice,
onComplete: (Int) -> Unit
) {
2020-07-29 16:16:29 -07:00
SLogging.info("Starting bonding for ${device.anonymize}")
// 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
)
SLogging.debug("Received bond state changed $state")
if (state != BOND_BONDING) {
context.unregisterReceiver(this) // we stay registered until bonding completes (either with BONDED or NONE)
2020-04-23 09:03:44 -07:00
SLogging.debug("Bonding completed, state=$state")
onComplete(state)
}
}
}
val filter = IntentFilter()
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
activity.registerReceiver(bondChangedReceiver, filter)
// We ignore missing BT adapters, because it lets us run on the emulator
device.createBond()
}
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
2021-02-21 11:34:43 +08:00
private val context: Context get() = 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
open class DeviceListEntry(val name: String, val address: String, val bonded: Boolean) {
2020-06-08 14:19:49 -07:00
val bluetoothAddress
get() =
if (isBluetooth)
2020-06-08 14:19:49 -07:00
address.substring(1)
else
null
override fun toString(): String {
return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize})"
}
val isBluetooth: Boolean get() = address[0] == 'x'
val isSerial: Boolean get() = address[0] == 's'
2020-02-18 08:56:53 -08:00
}
class USBDeviceListEntry(usbManager: UsbManager, val usb: UsbSerialDriver) : DeviceListEntry(
usb.device.deviceName,
SerialInterface.toInterfaceName(usb.device.deviceName),
SerialInterface.assumePermission || usbManager.hasPermission(usb.device)
)
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
val bluetoothAdapter = context.bluetoothManager?.adapter
private val usbManager get() = context.usbManager
2020-06-08 14:04:56 -07:00
var selectedAddress: String? = null
2020-04-08 21:17:23 -07:00
val errorText = object : MutableLiveData<String?>(null) {}
2020-04-09 11:03:17 -07:00
private var scanner: BluetoothLeScanner? = null
2020-04-08 21:17:23 -07:00
/// If this address is for a bluetooth device, return the macaddr portion, else null
val selectedBluetooth: String?
get() = selectedAddress?.let { a ->
if (a[0] == 'x')
a.substring(1)
else
null
}
/// If this address is for a USB device, return the macaddr portion, else null
val selectedUSB: String?
get() = selectedAddress?.let { a ->
if (a[0] == 's')
a.substring(1)
else
null
}
/// Use the string for the NopInterface
val selectedNotNull: String get() = selectedAddress ?: "n"
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"
2020-06-28 16:09:56 -07:00
errormsg(msg)
2020-04-09 11:03:17 -07:00
// 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) {
if ((result.device.name?.startsWith("Mesh") ?: false)) {
val addr = result.device.address
val fullAddr = "x$addr" // full address with the bluetooh prefix
// 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[fullAddr]
if (oldEntry == null || oldEntry.bonded != isBonded) { // Don't spam the GUI with endless updates for non changing nodes
val skipBogus = try {
// Note Soyes XS has a buggy BLE scan implementation and returns devices we didn't ask for. So we check to see if the
// last two chars of the name matches the last two of the address - if not we skip it
// Note: the address is always two more than the hex string we show in the name
// nasty parsing of a string that ends with ab:45 as four hex digits
val lastAddr = (addr.substring(
addr.length - 5,
addr.length - 3
) + addr.substring(addr.length - 2)).toInt(16)
val lastName =
result.device.name.substring(result.device.name.length - 4).toInt(16)
// ESP32 macaddr are two higher than the reported device name
// NRF52 macaddrs match the portion used in the string
// either would be acceptable
(lastAddr - 2) != lastName && lastAddr != lastName
} catch (ex: Throwable) {
false // If we fail parsing anything, don't do this nasty hack
}
if (skipBogus)
errormsg("Skipping bogus BLE entry $result")
else {
val entry = DeviceListEntry(
result.device.name
?: "unnamed-$addr", // autobug: some devices might not have a name, if someone is running really old device code?
fullAddr,
isBonded
)
debug("onScanResult ${entry}")
// If nothing was selected, by default select the first valid thing we see
2021-02-14 15:52:16 +08:00
val activity: MainActivity? = try {
GeeksvilleApplication.currentActivity as MainActivity? // Can be null if app is shutting down
2021-02-14 15:52:16 +08:00
} catch (_: ClassCastException) {
// Buggy "Z812" phones apparently have the wrong class type for this
errormsg("Unexpected class for main activity")
null
}
if (selectedAddress == null && entry.bonded && activity != null)
changeScanSelection(
activity,
fullAddr
)
addDevice(entry) // Add/replace entry
}
}
2020-02-18 09:09:49 -08:00
}
}
2020-04-09 11:03:17 -07:00
}
2020-02-13 19:02:40 -08:00
private fun addDevice(entry: DeviceListEntry) {
val oldDevs = devices.value!!
oldDevs[entry.address] = entry // Add/replace entry
devices.value = oldDevs // trigger gui updates
}
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
/**
* returns true if we could start scanning, false otherwise
*/
fun startScan(): Boolean {
2020-04-09 11:03:17 -07:00
debug("BTScan component active")
selectedAddress = RadioInterfaceService.getDeviceAddress(context)
2020-02-13 19:02:40 -08:00
2021-02-14 15:52:16 +08:00
return if (bluetoothAdapter == null || RadioInterfaceService.isMockInterfaceAvailable(
context
)
) {
2020-04-09 11:03:17 -07:00
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(
2021-02-01 10:31:39 +08:00
DeviceListEntry("Simulated interface", "m", true),
DeviceListEntry(context.getString(R.string.none), "n", true)
/* Don't populate fake bluetooth devices, because we don't want testlab inside of google
to try and use them.
DeviceListEntry("Meshtastic_ab12", "xaa", false),
DeviceListEntry("Meshtastic_32ac", "xbb", true) */
2020-04-09 11:03:17 -07:00
)
devices.value = (testnodes.map { it.address to it }).toMap().toMutableMap()
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 (selectedAddress == null)
changeScanSelection(
GeeksvilleApplication.currentActivity as MainActivity,
testnodes.first().address
)
true
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
val usbDrivers = SerialInterface.findDrivers(context)
/* model.bluetoothEnabled.value */
if (s == null && usbDrivers.isEmpty()) {
2020-04-09 11:03:17 -07:00
errorText.value =
2020-04-09 11:27:42 -07:00
context.getString(R.string.requires_bluetooth)
false
} else {
if (scanner == null) {
// Clear the old device list
devices.value?.clear()
// Include a placeholder for "None"
addDevice(DeviceListEntry(context.getString(R.string.none), "n", true))
usbDrivers.forEach { d ->
2020-06-08 14:04:56 -07:00
addDevice(
USBDeviceListEntry(usbManager, d)
2020-06-08 14:04:56 -07:00
)
}
if (s != null) { // could be null if bluetooth is disabled
debug("starting scan")
// filter and only accept devices that have our service
val filter =
ScanFilter.Builder()
2020-09-13 13:22:40 -07:00
// Samsung doesn't seem to filter properly by service so this can't work
// see https://stackoverflow.com/questions/57981986/altbeacon-android-beacon-library-not-working-after-device-has-screen-off-for-a-s/57995960#57995960
// and https://stackoverflow.com/a/45590493
// .setServiceUuid(ParcelUuid(BluetoothInterface.BTM_SERVICE_UUID))
.build()
val settings =
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
s.startScan(listOf(filter), settings, scanCallback)
scanner = s
}
} else {
debug("scan already running")
}
true
2020-04-08 21:17:23 -07:00
}
}
2020-04-09 11:03:17 -07:00
}
val devices = object : MutableLiveData<MutableMap<String, DeviceListEntry>>(mutableMapOf()) {
2020-04-09 11:03:17 -07:00
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
fun onSelected(activity: MainActivity, it: DeviceListEntry): 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) {
changeScanSelection(activity, it.address)
2020-04-08 21:17:23 -07:00
return true
} else {
2020-06-08 14:04:56 -07:00
// Handle requestng USB or bluetooth permissions for the device
debug("Requesting permissions for the device")
2020-06-08 14:04:56 -07:00
exceptionReporter {
val bleAddress = it.bluetoothAddress
if (bleAddress != null) {
// Request bonding for bluetooth
// We ignore missing BT adapters, because it lets us run on the emulator
bluetoothAdapter
?.getRemoteDevice(bleAddress)?.let { device ->
requestBonding(activity, device) { state ->
if (state == BOND_BONDED) {
errorText.value = activity.getString(R.string.pairing_completed)
changeScanSelection(
activity,
it.address
)
} else {
errorText.value =
activity.getString(R.string.pairing_failed_try_again)
}
2020-06-08 14:04:56 -07:00
// Force the GUI to redraw
devices.value = devices.value
}
}
}
2020-06-08 14:04:56 -07:00
}
2020-04-09 12:22:41 -07:00
2020-06-08 14:04:56 -07:00
if (it.isSerial) {
it as USBDeviceListEntry
2020-06-08 14:04:56 -07:00
val ACTION_USB_PERMISSION = "com.geeksville.mesh.USB_PERMISSION"
val usbReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (ACTION_USB_PERMISSION == intent.action) {
val device: UsbDevice =
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
2020-06-08 14:04:56 -07:00
if (intent.getBooleanExtra(
UsbManager.EXTRA_PERMISSION_GRANTED,
false
)
) {
info("User approved USB access")
changeScanSelection(activity, it.address)
2020-06-08 14:04:56 -07:00
// Force the GUI to redraw
devices.value = devices.value
2020-06-08 14:04:56 -07:00
} else {
errormsg("USB permission denied for device $device")
}
}
// We don't need to stay registered
activity.unregisterReceiver(this)
2020-04-08 21:17:23 -07:00
}
}
2020-06-08 14:04:56 -07:00
val permissionIntent =
PendingIntent.getBroadcast(activity, 0, Intent(ACTION_USB_PERMISSION), 0)
val filter = IntentFilter(ACTION_USB_PERMISSION)
activity.registerReceiver(usbReceiver, filter)
usbManager.requestPermission(it.usb.device, permissionIntent)
2020-06-08 14:04:56 -07:00
}
2020-04-08 21:17:23 -07:00
return false
}
}
/// Change to a new macaddr selection, updating GUI and radio
fun changeScanSelection(context: MainActivity, newAddr: String) {
try {
info("Changing device to ${newAddr.anonymize}")
changeDeviceSelection(context, newAddr)
selectedAddress =
newAddr // do this after changeDeviceSelection, so if it throws the change will be discarded
devices.value = devices.value // Force a GUI update
} catch (ex: RemoteException) {
errormsg("Failed talking to service, probably it is shutting down $ex.message")
// ignore the failure and the GUI won't be updating anyways
}
2020-04-08 21:17:23 -07:00
}
}
@SuppressLint("NewApi")
2020-04-08 21:17:23 -07:00
class SettingsFragment : ScreenFragment("Settings"), Logging {
private var _binding: SettingsFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
2020-04-08 21:17:23 -07:00
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
// FIXME - move this into a standard GUI helper class
private val guiJob = Job()
private val mainScope = CoroutineScope(Dispatchers.Main + guiJob)
private val hasCompanionDeviceApi: Boolean by lazy {
BluetoothInterface.hasCompanionDeviceApi(requireContext())
}
private val deviceManager: CompanionDeviceManager by lazy {
requireContext().getSystemService(CompanionDeviceManager::class.java)
}
override fun onDestroy() {
guiJob.cancel()
super.onDestroy()
}
private fun doFirmwareUpdate() {
model.meshService?.let { service ->
debug("User started firmware update")
binding.updateFirmwareButton.isEnabled = false // Disable until things complete
binding.updateProgressBar.visibility = View.VISIBLE
binding.updateProgressBar.progress = 0 // start from scratch
exceptionToSnackbar(requireView()) {
// We rely on our broadcast receiver to show progress as this progresses
service.startFirmwareUpdate()
}
}
}
2020-04-08 21:17:23 -07:00
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = SettingsFragmentBinding.inflate(inflater, container, false)
return binding.root
2020-04-08 21:17:23 -07:00
}
/// Set the correct update button configuration based on current progress
private fun refreshUpdateButton() {
debug("Reiniting the udpate button")
2020-05-15 10:18:15 -07:00
val info = model.myNodeInfo.value
val service = model.meshService
if (model.isConnected.value == MeshService.ConnectionState.CONNECTED && info != null && info.shouldUpdate && info.couldUpdate && service != null) {
binding.updateFirmwareButton.visibility = View.VISIBLE
binding.updateFirmwareButton.text =
2020-05-15 10:18:15 -07:00
getString(R.string.update_to).format(getString(R.string.cur_firmware_version))
val progress = service.updateStatus
binding.updateFirmwareButton.isEnabled =
(progress < 0) // if currently doing an upgrade disable button
if (progress >= 0) {
binding.updateProgressBar.progress = progress // update partial progress
binding.scanStatusText.setText(R.string.updating_firmware)
binding.updateProgressBar.visibility = View.VISIBLE
} else
when (progress) {
ProgressSuccess -> {
binding.scanStatusText.setText(R.string.update_successful)
binding.updateProgressBar.visibility = View.GONE
}
ProgressNotStarted -> {
// Do nothing - because we don't want to overwrite the status text in this case
binding.updateProgressBar.visibility = View.GONE
}
else -> {
binding.scanStatusText.setText(R.string.update_failed)
binding.updateProgressBar.visibility = View.VISIBLE
}
}
binding.updateProgressBar.isEnabled = false
2020-05-15 10:18:15 -07:00
} else {
binding.updateFirmwareButton.visibility = View.GONE
binding.updateProgressBar.visibility = View.GONE
2020-05-15 10:18:15 -07:00
}
}
private fun initNodeInfo() {
val connected = model.isConnected.value
refreshUpdateButton()
// If actively connected possibly let the user update firmware
val info = model.myNodeInfo.value
2020-05-15 10:18:15 -07:00
when (connected) {
MeshService.ConnectionState.CONNECTED -> {
val fwStr = info?.firmwareString ?: ""
binding.scanStatusText.text = getString(R.string.connected_to).format(fwStr)
2020-05-15 10:18:15 -07:00
}
MeshService.ConnectionState.DISCONNECTED ->
binding.scanStatusText.text = getString(R.string.not_connected)
2020-05-15 10:18:15 -07:00
MeshService.ConnectionState.DEVICE_SLEEP ->
binding.scanStatusText.text = getString(R.string.connected_sleeping)
2020-05-15 10:18:15 -07:00
}
}
2021-03-04 09:08:29 +08:00
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener{
override fun onItemSelected(
parent: AdapterView<*>,
view: View,
position: Int,
id: Long
) {
val item = parent.getItemAtPosition(position)
//TODO("Not yet implemented")
}
override fun onNothingSelected(parent: AdapterView<*>?) {
//TODO("Not yet implemented")
}
}
/// Setup the ui widgets unrelated to BLE scanning
private fun initCommonUI() {
2021-03-03 13:51:33 +08:00
val regions = arrayOf("US", "CN", "EU488")
val spinner = binding.regionSpinner
val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
2021-03-03 14:43:29 +08:00
// spinner.adapter = regionAdapter
2021-03-03 13:51:33 +08:00
2021-03-04 09:08:29 +08:00
spinner.onItemSelectedListener = regionSpinnerListener
2021-02-05 21:29:28 -08:00
model.ownerName.observe(viewLifecycleOwner, { name ->
binding.usernameEditText.setText(name)
2020-04-09 11:03:17 -07:00
})
2020-05-13 17:00:23 -07:00
// Only let user edit their name or set software update while connected to a radio
2021-02-05 21:29:28 -08:00
model.isConnected.observe(viewLifecycleOwner, Observer { connectionState ->
val connected = connectionState == MeshService.ConnectionState.CONNECTED
2021-03-03 13:51:33 +08:00
binding.nodeSettings.visibility = if(connected) View.VISIBLE else View.GONE
2021-02-05 21:29:28 -08:00
if (connectionState == MeshService.ConnectionState.DISCONNECTED)
model.ownerName.value = ""
2021-02-05 21:29:28 -08:00
2020-05-15 10:18:15 -07:00
initNodeInfo()
})
2020-05-13 17:00:23 -07:00
2020-05-15 10:18:15 -07:00
// Also watch myNodeInfo because it might change later
model.myNodeInfo.observe(viewLifecycleOwner, Observer {
initNodeInfo()
})
binding.updateFirmwareButton.setOnClickListener {
doFirmwareUpdate()
2020-05-13 17:00:23 -07:00
}
binding.usernameEditText.on(EditorInfo.IME_ACTION_DONE) {
2020-04-09 11:03:17 -07:00
debug("did IME action")
val n = binding.usernameEditText.text.toString().trim()
2020-04-09 11:03:17 -07:00
if (n.isNotEmpty())
model.setOwner(n)
2020-04-09 11:03:17 -07:00
requireActivity().hideKeyboard()
}
2021-02-05 21:29:28 -08:00
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
binding.analyticsOkayCheckbox.isChecked = app.isAnalyticsAllowed
2020-04-15 14:10:40 -07:00
binding.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
binding.reportBugButton.isEnabled = app.isAnalyticsAllowed
2020-04-15 14:10:40 -07:00
}
// report bug button only enabled if analytics is allowed
binding.reportBugButton.isEnabled = app.isAnalyticsAllowed
binding.reportBugButton.setOnClickListener {
2020-04-15 14:10:40 -07:00
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()
2020-04-09 11:03:17 -07:00
}
}
private fun addDeviceButton(device: BTScanModel.DeviceListEntry, enabled: Boolean) {
val b = RadioButton(requireActivity())
b.text = device.name
b.id = View.generateViewId()
b.isEnabled = enabled
b.isChecked =
device.address == scanModel.selectedNotNull && device.bonded // Only show checkbox if device is still paired
binding.deviceRadioGroup.addView(b)
// Once we have at least one device, don't show the "looking for" animation - it makes uers think
// something is busted
binding.scanProgressBar.visibility = View.INVISIBLE
b.setOnClickListener {
if (!device.bonded) // If user just clicked on us, try to bond
binding.scanStatusText.setText(R.string.starting_pairing)
b.isChecked =
scanModel.onSelected(requireActivity() as MainActivity, device)
if (!b.isSelected)
binding.scanStatusText.setText(getString(R.string.please_pair))
}
}
/// Show the GUI for classic scanning
private fun showClassicWidgets(visible: Int) {
binding.scanProgressBar.visibility = visible
binding.deviceRadioGroup.visibility = visible
}
2021-02-14 15:52:16 +08:00
private fun updateDevicesButtons(devices: MutableMap<String, BTScanModel.DeviceListEntry>?) {
// Remove the old radio buttons and repopulate
binding.deviceRadioGroup.removeAllViews()
2021-02-14 15:52:16 +08:00
if (devices == null) return
val adapter = scanModel.bluetoothAdapter
var hasShownOurDevice = false
devices.values.forEach { device ->
if (device.address == scanModel.selectedNotNull)
hasShownOurDevice = true
addDeviceButton(device, true)
}
2021-02-02 19:01:11 -08:00
// The selected device is not in the scan; it is either offline, or it doesn't advertise
// itself (most BLE devices don't advertise when connected).
// Show it in the list, greyed out based on connection status.
if (!hasShownOurDevice) {
// Note: we pull this into a tempvar, because otherwise some other thread can change selectedAddress after our null check
// and before use
val bleAddr = scanModel.selectedBluetooth
if (bleAddr != null && adapter != null && adapter.isEnabled) {
val bDevice =
adapter.getRemoteDevice(bleAddr)
if (bDevice.name != null) { // ignore nodes that node have a name, that means we've lost them since they appeared
val curDevice = BTScanModel.DeviceListEntry(
bDevice.name,
scanModel.selectedAddress!!,
bDevice.bondState == BOND_BONDED
)
2021-02-14 15:52:16 +08:00
addDeviceButton(
curDevice,
model.isConnected.value == MeshService.ConnectionState.CONNECTED
)
}
} else if (scanModel.selectedUSB != null) {
// Must be a USB device, show a placeholder disabled entry
val curDevice = BTScanModel.DeviceListEntry(
scanModel.selectedUSB!!,
scanModel.selectedAddress!!,
false
)
addDeviceButton(curDevice, false)
}
}
val hasBonded =
RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null
// get rid of the warning text once at least one device is paired.
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
2021-02-14 15:52:16 +08:00
binding.warningNotPaired.visibility =
if (hasBonded && !RadioInterfaceService.isMockInterfaceAvailable(requireContext())) View.GONE else View.VISIBLE
}
/// Setup the GUI to do a classic (pre SDK 26 BLE scan)
private fun initClassicScan() {
// Turn off the widgets for the new API (we turn on/off hte classic widgets when we start scanning
binding.changeRadioButton.visibility = View.GONE
2020-04-09 11:03:17 -07:00
showClassicWidgets(View.VISIBLE)
model.bluetoothEnabled.observe(viewLifecycleOwner, Observer { enabled ->
if (enabled)
scanModel.startScan()
else
scanModel.stopScan()
})
2020-04-08 21:34:57 -07:00
scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg ->
if (errMsg != null) {
binding.scanStatusText.text = errMsg
2020-04-08 21:34:57 -07:00
}
})
scanModel.devices.observe(
viewLifecycleOwner,
Observer { devices -> updateDevicesButtons(devices) })
model.isConnected.observe(
viewLifecycleOwner,
{ updateDevicesButtons(scanModel.devices.value) })
2020-04-08 21:17:23 -07:00
}
/// Start running the modern scan, once it has one result we enable the
private fun startBackgroundScan() {
// Disable the change button until our scan has some results
binding.changeRadioButton.isEnabled = false
// To skip filtering based on name and supported feature flags (UUIDs),
// don't include calls to setNamePattern() and addServiceUuid(),
// respectively. This example uses Bluetooth.
2020-07-15 15:58:53 -07:00
// We only look for Mesh (rather than the full name) because NRF52 uses a very short name
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
2020-07-15 15:58:53 -07:00
.setNamePattern(Pattern.compile("Mesh.*"))
// .addServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID), null)
.build()
// The argument provided in setSingleDevice() determines whether a single
// device name or a list of device names is presented to the user as
// pairing options.
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(false)
.build()
2020-05-29 13:58:38 -07:00
//val mainActivity = requireActivity() as MainActivity
// When the app tries to pair with the Bluetooth device, show the
// appropriate pairing request dialog to the user.
deviceManager.associate(
pairingRequest,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
debug("Found one device - enabling button")
binding.changeRadioButton.isEnabled = true
binding.changeRadioButton.setOnClickListener {
debug("User clicked BLE change button")
// Request code seems to be ignored anyways
startIntentSenderForResult(
chooserLauncher,
MainActivity.RC_SELECT_DEVICE, null, 0, 0, 0, null
)
}
}
override fun onFailure(error: CharSequence?) {
warn("BLE selection service failed $error")
// changeDeviceSelection(mainActivity, null) // deselect any device
}
}, null
)
}
private fun initModernScan() {
// Turn off the widgets for the classic API
binding.scanProgressBar.visibility = View.GONE
binding.deviceRadioGroup.visibility = View.GONE
binding.changeRadioButton.visibility = View.VISIBLE
val curRadio = RadioInterfaceService.getBondedDeviceAddress(requireContext())
if (curRadio != null) {
binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio)
binding.changeRadioButton.text = getString(R.string.change_radio)
} else {
binding.scanStatusText.text = getString(R.string.not_paired_yet)
binding.changeRadioButton.setText(R.string.select_radio)
}
startBackgroundScan()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initCommonUI()
if (hasCompanionDeviceApi)
initModernScan()
else
initClassicScan()
}
/**
* If the user has not turned on location access throw up a toast warning
*/
private fun checkLocationEnabled() {
2020-07-15 15:58:53 -07:00
// If they don't have google play FIXME for now we don't check for location access
if (isGooglePlayAvailable(requireContext())) {
// We do this painful process because LocationManager.isEnabled is only SDK28 or latet
val builder = LocationSettingsRequest.Builder()
builder.setNeedBle(true)
val request = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
builder.addLocationRequest(request) // Make sure we are granted high accuracy permission
2020-07-15 15:58:53 -07:00
val locationSettingsResponse = LocationServices.getSettingsClient(requireActivity())
.checkLocationSettings(builder.build())
2020-07-15 15:58:53 -07:00
locationSettingsResponse.addOnSuccessListener {
debug("We have location access")
}
2020-11-16 15:55:07 +08:00
locationSettingsResponse.addOnFailureListener { _ ->
2020-07-15 15:58:53 -07:00
errormsg("Failed to get location access")
// We always show the toast regardless of what type of exception we receive. Because even non
// resolvable api exceptions mean user still needs to fix something.
///if (exception is ResolvableApiException) {
// Location settings are not satisfied, but this can be fixed
// by showing the user a dialog.
// Show the dialog by calling startResolutionForResult(),
// and check the result in onActivityResult().
// exception.startResolutionForResult(this@MainActivity, REQUEST_CHECK_SETTINGS)
2020-07-15 15:58:53 -07:00
// For now just punt and show a dialog
// The context might be gone (if activity is going away) by the time this handler is called
context?.let { c ->
Toast.makeText(
c,
getString(R.string.location_disabled_warning),
Toast.LENGTH_SHORT
).show()
}
2020-07-15 15:58:53 -07:00
//} else
// Exceptions.report(exception)
}
}
}
private val updateProgressFilter = IntentFilter(ACTION_UPDATE_PROGRESS)
private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
refreshUpdateButton()
}
}
override fun onPause() {
super.onPause()
scanModel.stopScan()
requireActivity().unregisterReceiver(updateProgressReceiver)
}
2020-04-09 11:03:17 -07:00
override fun onResume() {
super.onResume()
if (!hasCompanionDeviceApi)
scanModel.startScan()
requireActivity().registerReceiver(updateProgressReceiver, updateProgressFilter)
// Keep reminding user BLE is still off
val hasUSB = activity?.let { SerialInterface.findDrivers(it).isNotEmpty() } ?: true
if (!hasUSB) {
// Warn user if BLE is disabled
if (scanModel.bluetoothAdapter?.isEnabled != true) {
Toast.makeText(
requireContext(),
R.string.error_bluetooth,
Toast.LENGTH_SHORT
).show()
} else {
checkLocationEnabled()
}
}
2020-02-13 09:25:39 -08:00
}
}
2020-04-09 11:03:17 -07:00