kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Merge remote-tracking branch 'remotes/origin/master'
commit
0acf037000
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
Ładowanie…
Reference in New Issue