diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 272a28943..2e1dd63ad 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -78,7 +78,7 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 3373ee423..468396d5e 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -451,7 +451,7 @@ class MainActivity : BaseActivity(), Logging,
tab.icon = ContextCompat.getDrawable(this, tabInfos[position].icon)
}.attach()
- model.isConnected.observe(this) { connected ->
+ model.connectionState.observe(this) { connected ->
updateConnectionStatusImage(connected)
}
@@ -516,7 +516,7 @@ class MainActivity : BaseActivity(), Logging,
requestedChannelUrl = appLinkData
// if the device is connected already, process it now
- if (model.isConnected.value == MeshService.ConnectionState.CONNECTED)
+ if (model.isConnected())
perhapsChangeChannel()
// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel
@@ -629,7 +629,7 @@ class MainActivity : BaseActivity(), Logging,
/// Called when we gain/lose a connection to our mesh radio
private fun onMeshConnectionChanged(newConnection: MeshService.ConnectionState) {
- val oldConnection = model.isConnected.value!!
+ val oldConnection = model.connectionState.value!!
debug("connchange $oldConnection -> $newConnection")
if (newConnection == MeshService.ConnectionState.CONNECTED) {
@@ -889,7 +889,7 @@ class MainActivity : BaseActivity(), Logging,
connectionJob = null
}
- debug("connected to mesh service, isConnected=${model.isConnected.value}")
+ debug("connected to mesh service, connectionState=${model.connectionState.value}")
}
}
@@ -983,7 +983,7 @@ class MainActivity : BaseActivity(), Logging,
menuInflater.inflate(R.menu.menu_main, menu)
model.actionBarMenu = menu
- updateConnectionStatusImage(model.isConnected.value!!)
+ updateConnectionStatusImage(model.connectionState.value!!)
return true
}
diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
index 11b9e9f82..61f4f1cc7 100644
--- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
+++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
@@ -1,24 +1,43 @@
package com.geeksville.mesh.android
import android.Manifest
+import android.annotation.SuppressLint
import android.app.NotificationManager
import android.bluetooth.BluetoothManager
+import android.companion.CompanionDeviceManager
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.usb.UsbManager
import android.os.Build
import androidx.core.content.ContextCompat
-import com.geeksville.mesh.repository.radio.BluetoothInterface
+import com.geeksville.android.GeeksvilleApplication
+import com.geeksville.mesh.MainActivity
/**
* @return null on platforms without a BlueTooth driver (i.e. the emulator)
*/
val Context.bluetoothManager: BluetoothManager? get() = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager?
+val Context.deviceManager: CompanionDeviceManager?
+ @SuppressLint("InlinedApi")
+ get() {
+ val activity: MainActivity? = GeeksvilleApplication.currentActivity as MainActivity?
+ return if (hasCompanionDeviceApi()) activity?.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager?
+ else null
+ }
+
val Context.usbManager: UsbManager get() = requireNotNull(getSystemService(Context.USB_SERVICE) as? UsbManager?) { "USB_SERVICE is not available"}
val Context.notificationManager: NotificationManager get() = requireNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager?)
+/**
+ * @return true if CompanionDeviceManager API is present
+ */
+fun Context.hasCompanionDeviceApi(): Boolean =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+ packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
+ else false
+
/**
* return a list of the permissions we don't have
*/
@@ -62,7 +81,7 @@ fun Context.getScanPermissions(): List {
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
}
*/
- if (!BluetoothInterface.hasCompanionDeviceApi(this)) {
+ if (!hasCompanionDeviceApi()) {
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
}
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index 38f487fd7..4536fffdb 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -92,9 +92,9 @@ class UIViewModel @Inject constructor(
/// Connection state to our radio device
private val _connectionState = MutableLiveData(MeshService.ConnectionState.DISCONNECTED)
- val isConnected: LiveData get() = _connectionState
+ val connectionState: LiveData get() = _connectionState
- // fun isConnected() = _connectionState.value == MeshService.ConnectionState.CONNECTED
+ fun isConnected() = _connectionState.value == MeshService.ConnectionState.CONNECTED
fun setConnectionState(connectionState: MeshService.ConnectionState) {
_connectionState.value = connectionState
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt
index 0ceaafe27..869dda64b 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt
@@ -122,12 +122,6 @@ class BluetoothInterface(
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): Boolean {
- /* val allPaired = if (hasCompanionDeviceApi(context)) {
- val deviceManager: CompanionDeviceManager by lazy {
- context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
- }
- deviceManager.associations.map { it }.toSet()
- } else { */
val allPaired = getBluetoothAdapter(context)?.bondedDevices.orEmpty()
.map { it.address }.toSet()
return if (!allPaired.contains(rest)) {
@@ -137,63 +131,6 @@ class BluetoothInterface(
true
}
-
- /// Return the device we are configured to use, or null for none
- /*
- @SuppressLint("NewApi")
- fun getBondedDeviceAddress(context: Context): String? =
- if (hasCompanionDeviceApi(context)) {
- // Use new companion API
-
- val deviceManager = context.getSystemService(CompanionDeviceManager::class.java)
- val associations = deviceManager.associations
- val result = associations.firstOrNull()
- debug("reading bonded devices: $result")
- result
- } else {
- // Use classic API and a preferences string
-
- val allPaired =
- getBluetoothAdapter(context)?.bondedDevices.orEmpty().map { it.address }.toSet()
-
- // If the user has unpaired our device, treat things as if we don't have one
- val address = InterfaceService.getPrefs(context).getString(DEVADDR_KEY, null)
-
- if (address != null && !allPaired.contains(address)) {
- warn("Ignoring stale bond to ${address.anonymize}")
- null
- } else
- address
- }
-*/
-
- /// Can we use the modern BLE scan API?
- fun hasCompanionDeviceApi(context: Context): Boolean =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val res =
- context.packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
- debug("CompanionDevice API available=$res")
- res
- } else {
- warn("CompanionDevice API not available, falling back to classic scan")
- false
- }
-
- /** FIXME - when adding companion device support back in, use this code to set companion device from setBondedDevice
- * if (BluetoothInterface.hasCompanionDeviceApi(this)) {
- // We only keep an association to one device at a time...
- if (addr != null) {
- val deviceManager = getSystemService(CompanionDeviceManager::class.java)
-
- deviceManager.associations.forEach { old ->
- if (addr != old) {
- BluetoothInterface.debug("Forgetting old BLE association $old")
- deviceManager.disassociate(old)
- }
- }
- }
- */
-
/**
* this is created in onCreate()
* We do an ugly hack of keeping it in the singleton so we can share it for the rare software update case
diff --git a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt
index 281d291b3..daa81b1e8 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt
@@ -46,7 +46,7 @@ class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging {
binding.lsSleepSwitch.isChecked = model.isPowerSaving ?: false
}
- model.isConnected.observe(viewLifecycleOwner) { connectionState ->
+ model.connectionState.observe(viewLifecycleOwner) { connectionState ->
val connected = connectionState == MeshService.ConnectionState.CONNECTED
binding.positionBroadcastPeriodView.isEnabled = connected && !model.locationShareDisabled
binding.lsSleepView.isEnabled = connected && model.isPowerSaving ?: false
diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
index 27dfa8148..f250b7cd9 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -13,6 +13,7 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
import android.widget.ImageView
+import androidx.activity.result.ActivityResultLauncher
import androidx.fragment.app.activityViewModels
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
@@ -28,11 +29,12 @@ import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.ChannelOption
import com.geeksville.mesh.model.ChannelSet
import com.geeksville.mesh.model.UIViewModel
-import com.geeksville.mesh.service.MeshService
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.protobuf.ByteString
-import com.google.zxing.integration.android.IntentIntegrator
+import com.journeyapps.barcodescanner.ScanContract
+import com.journeyapps.barcodescanner.ScanIntentResult
+import com.journeyapps.barcodescanner.ScanOptions
import dagger.hilt.android.AndroidEntryPoint
import java.security.SecureRandom
@@ -65,7 +67,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
- ): View? {
+ ): View {
_binding = ChannelFragmentBinding.inflate(inflater, container, false)
return binding.root
}
@@ -89,13 +91,13 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
private fun setGUIfromModel() {
val channels = model.channels.value
val channel = channels?.primaryChannel
-
- val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
+ val connected = model.isConnected()
// Only let buttons work if we are connected to the radio
+ binding.editableCheckbox.isChecked = false // start locked
+ onEditingChanged() // we just locked the gui
binding.shareButton.isEnabled = connected
- binding.editableCheckbox.isChecked = false // start locked
if (channel != null) {
binding.qrView.visibility = View.VISIBLE
binding.channelNameEdit.visibility = View.VISIBLE
@@ -123,7 +125,6 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
binding.editableCheckbox.isEnabled = false
}
- onEditingChanged() // we just locked the gui
val modemConfigs = ChannelOption.values()
val modemConfigList = modemConfigs.map { getString(it.configRes) }
val adapter = ArrayAdapter(
@@ -195,7 +196,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
requireActivity().hideKeyboard()
}
- binding.resetButton.setOnClickListener { _ ->
+ binding.resetButton.setOnClickListener {
// User just locked it, we should warn and then apply changes to radio
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.reset_to_defaults)
@@ -213,12 +214,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
binding.scanButton.setOnClickListener {
if ((requireActivity() as MainActivity).hasCameraPermission()) {
debug("Starting QR code scanner")
- val zxingScan = IntentIntegrator.forSupportFragment(this)
+ val zxingScan = ScanOptions()
zxingScan.setCameraId(0)
zxingScan.setPrompt("")
zxingScan.setBeepEnabled(false)
- zxingScan.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
- zxingScan.initiateScan()
+ zxingScan.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
+ barcodeLauncher.launch(zxingScan)
} else {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.camera_required)
@@ -234,7 +235,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
}
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
- binding.editableCheckbox.setOnClickListener { _ ->
+ binding.editableCheckbox.setOnClickListener {
/// We use this to determine if the user tried to install a custom name
var originalName = ""
@@ -299,14 +300,14 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
shareChannel()
}
- model.channels.observe(viewLifecycleOwner, {
+ model.channels.observe(viewLifecycleOwner) {
setGUIfromModel()
- })
+ }
// If connection state changes, we might need to enable/disable buttons
- model.isConnected.observe(viewLifecycleOwner, {
+ model.connectionState.observe(viewLifecycleOwner) {
setGUIfromModel()
- })
+ }
}
private fun getModemConfig(selectedChannelOptionString: String): ChannelProtos.ChannelSettings.ModemConfig {
@@ -318,14 +319,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
return ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED
}
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
- if (result != null) {
- if (result.contents != null) {
- ((requireActivity() as MainActivity).perhapsChangeChannel(Uri.parse(result.contents)))
- }
- } else {
- super.onActivityResult(requestCode, resultCode, data)
+ // Register the launcher and result handler
+ private val barcodeLauncher: ActivityResultLauncher = registerForActivityResult(
+ ScanContract()
+ ) { result: ScanIntentResult ->
+ if (result.contents != null) {
+ ((requireActivity() as MainActivity).perhapsChangeChannel(Uri.parse(result.contents)))
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
index 4847f0691..cbb6350bd 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
@@ -294,7 +294,7 @@ class MessagesFragment : Fragment(), Logging {
}
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
- model.isConnected.observe(viewLifecycleOwner) { connectionState ->
+ model.connectionState.observe(viewLifecycleOwner) { connectionState ->
// If we don't know our node ID and we are offline don't let user try to send
val connected = connectionState == MeshService.ConnectionState.CONNECTED
binding.textInputLayout.isEnabled = connected
diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
index 9385f62ea..200457cca 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -1,7 +1,6 @@
package com.geeksville.mesh.ui
import android.annotation.SuppressLint
-import android.app.Activity
import android.app.Application
import android.app.PendingIntent
import android.bluetooth.BluetoothDevice
@@ -21,8 +20,11 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.*
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
@@ -36,7 +38,6 @@ import com.geeksville.mesh.android.*
import com.geeksville.mesh.databinding.SettingsFragmentBinding
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
-import com.geeksville.mesh.repository.radio.BluetoothInterface
import com.geeksville.mesh.repository.radio.MockInterface
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.radio.SerialInterface
@@ -134,7 +135,7 @@ class BTScanModel @Inject constructor(
null
override fun toString(): String {
- return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize})"
+ return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize}, bonded=$bonded)"
}
val isBluetooth: Boolean get() = address[0] == 'x'
@@ -152,7 +153,10 @@ class BTScanModel @Inject constructor(
debug("BTScanModel cleared")
}
- val bluetoothAdapter = context.bluetoothManager?.adapter
+ private val bluetoothAdapter = context.bluetoothManager?.adapter
+ private val deviceManager get() = context.deviceManager
+ val hasCompanionDeviceApi get() = context.hasCompanionDeviceApi()
+ private val hasConnectPermission get() = context.hasConnectPermission()
private val usbManager get() = context.usbManager
var selectedAddress: String? = null
@@ -243,9 +247,11 @@ class BTScanModel @Inject constructor(
scanner?.stopScan(scanCallback)
} catch (ex: Throwable) {
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
+ } finally {
+ scanner = null
+ _spinner.value = false
}
- scanner = null
- }
+ } else _spinner.value = false
}
/**
@@ -297,6 +303,9 @@ class BTScanModel @Inject constructor(
// Include a placeholder for "None"
addDevice(DeviceListEntry(context.getString(R.string.none), "n", true))
+ // Include CompanionDeviceManager valid associations
+ addDeviceAssociations()
+
serialDevices.forEach { (_, d) ->
addDevice(USBDeviceListEntry(usbManager, d))
}
@@ -308,13 +317,20 @@ class BTScanModel @Inject constructor(
}
}
+ fun startScan () {
+ if (hasCompanionDeviceApi) {
+ startCompanionScan()
+ } else startClassicScan()
+ }
+
@SuppressLint("MissingPermission")
- fun startScan() {
+ private fun startClassicScan() {
/// The following call might return null if the user doesn't have bluetooth access permissions
val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner != null) { // could be null if bluetooth is disabled
- debug("starting scan")
+ debug("starting classic scan")
+ _spinner.value = true
// filter and only accept devices that have our service
val filter =
@@ -333,6 +349,94 @@ class BTScanModel @Inject constructor(
}
}
+ /**
+ * @return DeviceListEntry from Bluetooth Address.
+ * Only valid if name begins with "Meshtastic"...
+ */
+ @SuppressLint("MissingPermission")
+ fun bleDeviceFrom(bleAddress: String): DeviceListEntry {
+ val device =
+ if (hasConnectPermission) bluetoothAdapter?.getRemoteDevice(bleAddress) else null
+
+ return if (device != null && device.name != null) {
+ DeviceListEntry(
+ device.name,
+ "x${device.address}", // full address with the bluetooth prefix added
+ device.bondState == BOND_BONDED
+ )
+ } else DeviceListEntry("", "", false)
+ }
+
+ @SuppressLint("NewApi")
+ private fun addDeviceAssociations() {
+ if (hasCompanionDeviceApi) deviceManager?.associations?.forEach { bleAddress ->
+ val bleDevice = bleDeviceFrom(bleAddress)
+ if (!bleDevice.bonded) { // Clean up associations after pairing is removed
+ debug("Forgetting old BLE association ${bleAddress.anonymize}")
+ deviceManager?.disassociate(bleAddress)
+ } else if (bleDevice.name.startsWith("Mesh")) {
+ addDevice(bleDevice)
+ }
+ }
+ }
+
+ private val _spinner = MutableLiveData(false)
+ val spinner: LiveData get() = _spinner
+
+ private val _associationRequest = MutableLiveData(null)
+ val associationRequest: LiveData get() = _associationRequest
+
+ /**
+ * Called immediately after fragment observes CompanionDeviceManager activity result
+ */
+ fun clearAssociationRequest() {
+ _associationRequest.value = null
+ }
+
+ @SuppressLint("NewApi")
+ private fun associationRequest(): AssociationRequest {
+ // To skip filtering based on name and supported feature flags (UUIDs),
+ // don't include calls to setNamePattern() and addServiceUuid(),
+ // respectively. This example uses Bluetooth.
+ // We only look for Mesh (rather than the full name) because NRF52 uses a very short name
+ val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
+ .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.
+ return AssociationRequest.Builder()
+ .addDeviceFilter(deviceFilter)
+ .setSingleDevice(false)
+ .build()
+ }
+
+ @SuppressLint("NewApi")
+ private fun startCompanionScan() {
+ debug("starting companion scan")
+ _spinner.value = true
+ deviceManager?.associate(
+ associationRequest(),
+ @SuppressLint("NewApi")
+ object : CompanionDeviceManager.Callback() {
+ override fun onDeviceFound(chooserLauncher: IntentSender) {
+ debug("CompanionDeviceManager - device found")
+ _spinner.value = false
+ chooserLauncher.let {
+ val request: IntentSenderRequest = IntentSenderRequest.Builder(it).build()
+ _associationRequest.value = request
+ }
+ }
+
+ override fun onFailure(error: CharSequence?) {
+ warn("BLE selection service failed $error")
+ }
+ }, null
+ )
+ }
+
val devices = object : MutableLiveData>(mutableMapOf()) {
/**
@@ -348,7 +452,7 @@ class BTScanModel @Inject constructor(
*/
override fun onInactive() {
super.onInactive()
- // stopScan()
+ stopScan()
}
}
@@ -465,14 +569,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
@Inject
internal lateinit var usbRepository: UsbRepository
- private val hasCompanionDeviceApi: Boolean by lazy {
- BluetoothInterface.hasCompanionDeviceApi(requireContext())
- }
-
- private val deviceManager: CompanionDeviceManager by lazy {
- requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
- }
-
private val myActivity get() = requireActivity() as MainActivity
override fun onDestroy() {
@@ -512,7 +608,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
debug("Reiniting the update button")
val info = model.myNodeInfo.value
val service = model.meshService
- if (model.isConnected.value == MeshService.ConnectionState.CONNECTED && info != null && info.shouldUpdate && info.couldUpdate && service != null) {
+ if (model.isConnected() && info != null && info.shouldUpdate && info.couldUpdate && service != null) {
binding.updateFirmwareButton.visibility = View.VISIBLE
binding.updateFirmwareButton.text =
getString(R.string.update_to).format(getString(R.string.short_firmware_version))
@@ -561,7 +657,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
* Pull the latest device info from the model and into the GUI
*/
private fun updateNodeInfo() {
- val connected = model.isConnected.value
+ val connected = model.connectionState.value
val isConnected = connected == MeshService.ConnectionState.CONNECTED
binding.nodeSettings.visibility = if (isConnected) View.VISIBLE else View.GONE
@@ -648,9 +744,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = regionAdapter
- bluetoothViewModel.enabled.observe(viewLifecycleOwner) {
- if (it) binding.changeRadioButton.show()
- else binding.changeRadioButton.hide()
+ bluetoothViewModel.enabled.observe(viewLifecycleOwner) { enabled ->
+ if (enabled) {
+ binding.changeRadioButton.show()
+ if (scanModel.devices.value.isNullOrEmpty()) scanModel.setupScan()
+ if (binding.scanStatusText.text == getString(R.string.requires_bluetooth)) updateNodeInfo()
+ } else binding.changeRadioButton.hide()
}
model.ownerName.observe(viewLifecycleOwner) { name ->
@@ -658,7 +757,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
// Only let user edit their name or set software update while connected to a radio
- model.isConnected.observe(viewLifecycleOwner) {
+ model.connectionState.observe(viewLifecycleOwner) {
updateNodeInfo()
updateDevicesButtons(scanModel.devices.value)
}
@@ -677,12 +776,28 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
updateNodeInfo()
}
+ scanModel.devices.observe(viewLifecycleOwner) { devices ->
+ updateDevicesButtons(devices)
+ }
+
scanModel.errorText.observe(viewLifecycleOwner) { errMsg ->
if (errMsg != null) {
binding.scanStatusText.text = errMsg
}
}
+ // show the spinner when [spinner] is true
+ scanModel.spinner.observe(viewLifecycleOwner) { show ->
+ binding.scanProgressBar.visibility = if (show) View.VISIBLE else View.GONE
+ }
+
+ scanModel.associationRequest.observe(viewLifecycleOwner) { request ->
+ request?.let {
+ associationResultLauncher.launch(request)
+ scanModel.clearAssociationRequest()
+ }
+ }
+
binding.updateFirmwareButton.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setMessage("${getString(R.string.update_firmware)}?")
@@ -765,8 +880,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
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
+ b.isChecked = device.address == scanModel.selectedNotNull
binding.deviceRadioGroup.addView(b)
b.setOnClickListener {
@@ -775,21 +889,15 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
b.isChecked =
scanModel.onSelected(myActivity, device)
-
- if (!b.isSelected) {
- binding.scanStatusText.text = getString(R.string.please_pair)
- }
}
}
- @SuppressLint("MissingPermission")
private fun updateDevicesButtons(devices: MutableMap?) {
// Remove the old radio buttons and repopulate
binding.deviceRadioGroup.removeAllViews()
if (devices == null) return
- val adapter = scanModel.bluetoothAdapter
var hasShownOurDevice = false
devices.values.forEach { device ->
if (device.address == scanModel.selectedNotNull)
@@ -805,18 +913,18 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// and before use
val bleAddr = scanModel.selectedBluetooth
- if (bleAddr != null && adapter != null && myActivity.hasConnectPermission()) {
- 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
+ if (bleAddr != null) {
+ debug("bleAddr= $bleAddr selected= ${scanModel.selectedAddress}")
+ val bleDevice = scanModel.bleDeviceFrom(bleAddr)
+ if (bleDevice.name.startsWith("Mesh")) { // 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
+ bleDevice.name,
+ bleDevice.address,
+ bleDevice.bonded
)
addDeviceButton(
curDevice,
- model.isConnected.value == MeshService.ConnectionState.CONNECTED
+ model.isConnected()
)
}
} else if (scanModel.selectedUSB != null) {
@@ -843,110 +951,56 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
- private fun initClassicScan() {
-
- scanModel.devices.observe(viewLifecycleOwner) { devices ->
- updateDevicesButtons(devices)
- }
-
- binding.changeRadioButton.setOnClickListener {
- debug("User clicked changeRadioButton")
- if (!myActivity.hasScanPermission()) {
- myActivity.requestScanPermission()
- } else {
- checkLocationEnabled()
- scanLeDevice()
- }
- }
- }
-
// per https://developer.android.com/guide/topics/connectivity/bluetooth/find-ble-devices
private fun scanLeDevice() {
var scanning = false
- val SCAN_PERIOD: Long = 5000 // Stops scanning after 5 seconds
+ val SCAN_PERIOD: Long = 10000 // Stops scanning after 10 seconds
if (!scanning) { // Stops scanning after a pre-defined scan period.
Handler(Looper.getMainLooper()).postDelayed({
scanning = false
- binding.scanProgressBar.visibility = View.GONE
scanModel.stopScan()
}, SCAN_PERIOD)
scanning = true
- binding.scanProgressBar.visibility = View.VISIBLE
scanModel.startScan()
} else {
scanning = false
- binding.scanProgressBar.visibility = View.GONE
scanModel.stopScan()
}
}
- private fun startCompanionScan() {
- // 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.
- // We only look for Mesh (rather than the full name) because NRF52 uses a very short name
- val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
- .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()
-
- // 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 changeRadioButton")
- binding.changeRadioButton.isEnabled = true
- binding.changeRadioButton.setOnClickListener {
- debug("User clicked changeRadioButton")
- try {
- startIntentSenderForResult(
- chooserLauncher,
- MainActivity.SELECT_DEVICE_REQUEST_CODE, null, 0, 0, 0, null
- )
- } catch (ex: Throwable) {
- errormsg("CompanionDevice startIntentSenderForResult error")
- }
- }
- }
-
- override fun onFailure(error: CharSequence?) {
- warn("BLE selection service failed $error")
- // changeDeviceSelection(myActivity, null) // deselect any device
- }
- }, null
- )
- }
-
- private fun initModernScan() {
-
- scanModel.devices.observe(viewLifecycleOwner) { devices ->
- updateDevicesButtons(devices)
- startCompanionScan()
- }
+ @SuppressLint("MissingPermission")
+ val associationResultLauncher = registerForActivityResult(
+ ActivityResultContracts.StartIntentSenderForResult()
+ ) {
+ it.data
+ ?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
+ ?.let { device ->
+ scanModel.onSelected(
+ myActivity,
+ BTScanModel.DeviceListEntry(
+ device.name,
+ "x${device.address}",
+ device.bondState == BOND_BONDED
+ )
+ )
+ }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initCommonUI()
- if (hasCompanionDeviceApi)
- initModernScan()
- else
- initClassicScan()
+
+ binding.changeRadioButton.setOnClickListener {
+ debug("User clicked changeRadioButton")
+ if (!myActivity.hasScanPermission()) {
+ myActivity.requestScanPermission()
+ } else {
+ if (!scanModel.hasCompanionDeviceApi) checkLocationEnabled()
+ scanLeDevice()
+ }
+ }
}
// If the user has not turned on location access throw up a toast warning
@@ -1053,7 +1107,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
val hasUSB = usbRepository.serialDevicesWithDrivers.value.isNotEmpty()
if (!hasUSB) {
// Warn user if BLE is disabled
- if (scanModel.bluetoothAdapter?.isEnabled != true) {
+ if (bluetoothViewModel.enabled.value == false) {
showSnackbar(getString(R.string.error_bluetooth))
} else {
if (binding.provideLocationCheckbox.isChecked)
@@ -1061,33 +1115,4 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
}
-
- @SuppressLint("MissingPermission")
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (hasCompanionDeviceApi && myActivity.hasConnectPermission()
- && requestCode == MainActivity.SELECT_DEVICE_REQUEST_CODE
- && resultCode == Activity.RESULT_OK
- ) {
- val deviceToPair: BluetoothDevice =
- data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)!!
-
- // We only keep an association to one device at a time...
- deviceManager.associations.forEach { old ->
- if (deviceToPair.address != old) {
- debug("Forgetting old BLE association ${old.anonymize}")
- deviceManager.disassociate(old)
- }
- }
- scanModel.onSelected(
- myActivity,
- BTScanModel.DeviceListEntry(
- deviceToPair.name,
- "x${deviceToPair.address}",
- deviceToPair.bondState == BOND_BONDED
- )
- )
- } else {
- super.onActivityResult(requestCode, resultCode, data)
- }
- }
}
diff --git a/app/src/main/proto b/app/src/main/proto
index a578453b3..79d24080f 160000
--- a/app/src/main/proto
+++ b/app/src/main/proto
@@ -1 +1 @@
-Subproject commit a578453b3c17794b61fb6cf4470ecaac8287d6d2
+Subproject commit 79d24080ff83b0a54bc1619f07f41f17ffedfb99
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 99f5076dd..a63c87642 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -56,7 +56,7 @@
Kapcsolódva a rádióhoz, de az alvó üzemmódban van
Frissítés %s verzióra
Az alkalmazás frissítése szükséges
- Frissítenie kell ezt az alkalmazást a Google Play áruházban (vagy a GitHub-ról), mert túl régi, hogy kommunikálni tudjob ezzel a rádió firmware-rel. Kérem olvassa el a tudnivalókat ebből a wiki-ből.
+ Frissítenie kell ezt az alkalmazást a Google Play áruházban (vagy a GitHub-ról), mert túl régi, hogy kommunikálni tudjob ezzel a rádió firmware-rel. Kérem olvassa el a tudnivalókat ebből a docs-ből.
Egyik sem (letiltás)
Rövid hatótáv (gyors)
Közepes hatótáv (gyors)
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index ca1e8b18d..bf12b3bd7 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -57,7 +57,7 @@ Jeśli jesteś zainteresowany opłaceniem przez nas mapboxa (lub przejściem do
Połączono z radiem w stanie uśpienia
Aktualizuj do %s
Konieczna aktualizacja aplikacji
- Należy zaktualizować aplikację za pomocą Sklepu Play lub Githuba, bo jest zbyt stara aby dogadać się z oprogramowaniem zainstalowanym na tym tadiu. Więcej informacji (ang.)
+ Należy zaktualizować aplikację za pomocą Sklepu Play lub Githuba, bo jest zbyt stara aby dogadać się z oprogramowaniem zainstalowanym na tym tadiu. Więcej informacji (ang.)
Brak (wyłącz)
Krótki zasięg / Szybko
Średni zasięg / Szybko
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 5c60f230b..8f91e4f93 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -57,7 +57,7 @@
Conectado ao rádio, mas ele está em suspensão (sleep)
Atualização para %s
Atualização do aplicativo necessária
- Será necessário atualizar este aplicativo no Google Play (ou Github). Versão muito antiga para comunicar com o firmware do rádio. Favor consultar wiki.
+ Será necessário atualizar este aplicativo no Google Play (ou Github). Versão muito antiga para comunicar com o firmware do rádio. Favor consultar docs.
Nenhum (desabilitado)
Curto alcance / rápido
Médio alcance / rápido
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index bc9aa1573..41ee01ff4 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -57,7 +57,7 @@
Pripojené k uspatému vysielaču.
Aktualizovať na %s
Aplikácia je príliš stará
- Musíte aktualizovať aplikáciu na Google Play store (alebo z Github). Je príliš stará pre komunikáciu s touto verziou firmvéru vysielača. Viac informácií k tejto téme nájdete na Meshtastic wiki.
+ Musíte aktualizovať aplikáciu na Google Play store (alebo z Github). Je príliš stará pre komunikáciu s touto verziou firmvéru vysielača. Viac informácií k tejto téme nájdete na Meshtastic docs.
Žiaden (zakázať)
Nie, ďakujem
Pripomenúť neskôr
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index b563eb2c5..ef65f5359 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -57,7 +57,7 @@
已连接到设备,正在休眠中
更新到%s
需要应用程序更新
- 您必须在 Google Play或Github上更新此应用程序.固件太旧,请阅读我们的 wiki 这个话题.
+ 您必须在 Google Play或Github上更新此应用程序.固件太旧,请阅读我们的 docs 这个话题.
无(禁用)
短距离(速度快)
中等距离(速度快)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4069e1acb..a3ecbf49e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -61,7 +61,7 @@
Connected to radio, but it is sleeping
Update to %s
Application update required
- You must update this application on the app store (or Github). It is too old to talk to this radio firmware. Please read our wiki on this topic.
+ You must update this application on the app store (or Github). It is too old to talk to this radio firmware. Please read our docs on this topic.
None (disable)
Short Range / Fast
Medium Range / Fast
@@ -142,4 +142,4 @@
System default
Map style
Resend
-
\ No newline at end of file
+
diff --git a/build.gradle b/build.gradle
index 8fe288c92..6679352e7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.6.20'
+ ext.kotlin_version = '1.6.21'
ext.coroutines_version = '1.6.0'
ext.room_version = '2.4.2'
ext.hilt_version = '2.40.5'
diff --git a/geeksville-androidlib b/geeksville-androidlib
index 379f76459..bcd9aa529 160000
--- a/geeksville-androidlib
+++ b/geeksville-androidlib
@@ -1 +1 @@
-Subproject commit 379f7645900c44e30d6b17e558bd36884d478b1b
+Subproject commit bcd9aa529719ad8a9203aa5bbf0a7d707aa4f325