Merge remote-tracking branch 'remotes/origin/master'

pull/420/head
Mike Cumings 2022-05-06 11:19:11 -07:00
commit 0acf037000
18 zmienionych plików z 240 dodań i 260 usunięć

Wyświetl plik

@ -78,7 +78,7 @@
<application
tools:replace="android:icon"
android:name="com.geeksville.mesh.MeshUtilApplication"
android:allowBackup="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher2"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher2_round"
@ -191,4 +191,4 @@
</receiver>
</application>
</manifest>
</manifest>

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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<String> {
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)
}

Wyświetl plik

@ -92,9 +92,9 @@ class UIViewModel @Inject constructor(
/// Connection state to our radio device
private val _connectionState = MutableLiveData(MeshService.ConnectionState.DISCONNECTED)
val isConnected: LiveData<MeshService.ConnectionState> get() = _connectionState
val connectionState: LiveData<MeshService.ConnectionState> get() = _connectionState
// fun isConnected() = _connectionState.value == MeshService.ConnectionState.CONNECTED
fun isConnected() = _connectionState.value == MeshService.ConnectionState.CONNECTED
fun setConnectionState(connectionState: MeshService.ConnectionState) {
_connectionState.value = connectionState

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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<ScanOptions> = registerForActivityResult(
ScanContract()
) { result: ScanIntentResult ->
if (result.contents != null) {
((requireActivity() as MainActivity).perhapsChangeChannel(Uri.parse(result.contents)))
}
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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<Boolean> get() = _spinner
private val _associationRequest = MutableLiveData<IntentSenderRequest?>(null)
val associationRequest: LiveData<IntentSenderRequest?> 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<MutableMap<String, DeviceListEntry>>(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<String, BTScanModel.DeviceListEntry>?) {
// 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<BluetoothDevice>(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)
}
}
}

@ -1 +1 @@
Subproject commit a578453b3c17794b61fb6cf4470ecaac8287d6d2
Subproject commit 79d24080ff83b0a54bc1619f07f41f17ffedfb99

Wyświetl plik

@ -56,7 +56,7 @@
<string name="connected_sleeping">Kapcsolódva a rádióhoz, de az alvó üzemmódban van</string>
<string name="update_to">Frissítés %s verzióra</string>
<string name="app_too_old">Az alkalmazás frissítése szükséges</string>
<string name="must_update">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 <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a>-ből.</string>
<string name="must_update">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 <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a>-ből.</string>
<string name="none">Egyik sem (letiltás)</string>
<string name="modem_config_short">Rövid hatótáv (gyors)</string>
<string name="modem_config_medium">Közepes hatótáv (gyors)</string>

Wyświetl plik

@ -57,7 +57,7 @@ Jeśli jesteś zainteresowany opłaceniem przez nas mapboxa (lub przejściem do
<string name="connected_sleeping">Połączono z radiem w stanie uśpienia</string>
<string name="update_to">Aktualizuj do %s</string>
<string name="app_too_old">Konieczna aktualizacja aplikacji</string>
<string name="must_update">Należy zaktualizować aplikację za pomocą Sklepu Play lub Githuba, bo jest zbyt stara aby dogadać się z oprogramowaniem zainstalowanym na tym tadiu. <a href="https://www.meshtastic.org/software/android-too-old.html">Więcej informacji (ang.)</a></string>
<string name="must_update">Należy zaktualizować aplikację za pomocą Sklepu Play lub Githuba, bo jest zbyt stara aby dogadać się z oprogramowaniem zainstalowanym na tym tadiu. <a href="https://meshtastic.org/docs/software/android/android-installation">Więcej informacji (ang.)</a></string>
<string name="none">Brak (wyłącz)</string>
<string name="modem_config_short">Krótki zasięg / Szybko</string>
<string name="modem_config_medium">Średni zasięg / Szybko</string>

Wyświetl plik

@ -57,7 +57,7 @@
<string name="connected_sleeping">Conectado ao rádio, mas ele está em suspensão (sleep)</string>
<string name="update_to">Atualização para %s</string>
<string name="app_too_old">Atualização do aplicativo necessária</string>
<string name="must_update">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 <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a>.</string>
<string name="must_update">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 <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a>.</string>
<string name="none">Nenhum (desabilitado)</string>
<string name="modem_config_short">Curto alcance / rápido</string>
<string name="modem_config_medium">Médio alcance / rápido</string>

Wyświetl plik

@ -57,7 +57,7 @@
<string name="connected_sleeping">Pripojené k uspatému vysielaču.</string>
<string name="update_to">Aktualizovať na %s</string>
<string name="app_too_old">Aplikácia je príliš stará</string>
<string name="must_update">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 <a href="https://www.meshtastic.org/software/android-too-old.html">Meshtastic wiki</a>.</string>
<string name="must_update">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 <a href="https://meshtastic.org/docs/software/android/android-installation">Meshtastic docs</a>.</string>
<string name="none">Žiaden (zakázať)</string>
<string name="rate_dialog_no_en">Nie, ďakujem</string>
<string name="rate_dialog_cancel_en">Pripomenúť neskôr</string>

Wyświetl plik

@ -57,7 +57,7 @@
<string name="connected_sleeping">已连接到设备,正在休眠中</string>
<string name="update_to">更新到%s</string>
<string name="app_too_old">需要应用程序更新</string>
<string name="must_update">您必须在 Google Play或Github上更新此应用程序.固件太旧,请阅读我们的 <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a> 这个话题.</string>
<string name="must_update">您必须在 Google Play或Github上更新此应用程序.固件太旧,请阅读我们的 <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a> 这个话题.</string>
<string name="none">无(禁用)</string>
<string name="modem_config_short">短距离(速度快)</string>
<string name="modem_config_medium">中等距离(速度快)</string>

Wyświetl plik

@ -61,7 +61,7 @@
<string name="connected_sleeping">Connected to radio, but it is sleeping</string>
<string name="update_to">Update to %s</string>
<string name="app_too_old">Application update required</string>
<string name="must_update">You must update this application on the app store (or Github). It is too old to talk to this radio firmware. Please read our <a href="https://www.meshtastic.org/software/android-too-old.html">wiki</a> on this topic.</string>
<string name="must_update">You must update this application on the app store (or Github). It is too old to talk to this radio firmware. Please read our <a href="https://meshtastic.org/docs/software/android/android-installation">docs</a> on this topic.</string>
<string name="none">None (disable)</string>
<string name="modem_config_short">Short Range / Fast</string>
<string name="modem_config_medium">Medium Range / Fast</string>
@ -142,4 +142,4 @@
<string name="preferences_system_default">System default</string>
<string name="preferences_map_style">Map style</string>
<string name="resend">Resend</string>
</resources>
</resources>

Wyświetl plik

@ -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'

@ -1 +1 @@
Subproject commit 379f7645900c44e30d6b17e558bd36884d478b1b
Subproject commit bcd9aa529719ad8a9203aa5bbf0a7d707aa4f325