kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
commit
b44a104be2
|
@ -30,15 +30,15 @@ android {
|
|||
keyPassword "$meshtasticKeyPassword"
|
||||
}
|
||||
} */
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
// leave undefined to use version plugin wants
|
||||
// buildToolsVersion "30.0.2" // Note: 30.0.2 doesn't yet work on Github actions CI
|
||||
defaultConfig {
|
||||
applicationId "com.geeksville.mesh"
|
||||
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
||||
targetSdkVersion 29
|
||||
versionCode 20211 // format is Mmmss (where M is 1+the numeric major number
|
||||
versionName "1.2.11"
|
||||
targetSdkVersion 30
|
||||
versionCode 20213 // format is Mmmss (where M is 1+the numeric major number
|
||||
versionName "1.2.13"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// per https://developer.android.com/studio/write/vector-asset-studio
|
||||
|
@ -151,7 +151,7 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
// For now I'm not using javalite, because I want JSON printing
|
||||
implementation ('com.google.protobuf:protobuf-java:3.15.5')
|
||||
implementation ('com.google.protobuf:protobuf-java:3.15.6')
|
||||
|
||||
// For UART access
|
||||
// implementation 'com.google.android.things:androidthings:1.0'
|
||||
|
@ -170,7 +170,7 @@ dependencies {
|
|||
implementation 'com.google.android.gms:play-services-auth:19.0.0'
|
||||
|
||||
// Add the Firebase SDK for Crashlytics.
|
||||
implementation 'com.google.firebase:firebase-crashlytics:17.3.1'
|
||||
implementation 'com.google.firebase:firebase-crashlytics:17.4.0'
|
||||
|
||||
// alas implementation bug deep in the bowels when I tried it for my SyncBluetoothDevice class
|
||||
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
|
||||
|
|
|
@ -113,4 +113,7 @@ interface IMeshService {
|
|||
Return a number 0-100 for progress. -1 for completed and success, -2 for failure
|
||||
*/
|
||||
int getUpdateStatus();
|
||||
|
||||
int getRegion();
|
||||
void setRegion(int regionCode);
|
||||
}
|
||||
|
|
|
@ -663,30 +663,32 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
|
||||
debug("Getting latest radioconfig from service")
|
||||
try {
|
||||
val info = service.myNodeInfo
|
||||
val info: MyNodeInfo? = service.myNodeInfo // this can be null
|
||||
model.myNodeInfo.value = info
|
||||
|
||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
||||
if (isOld)
|
||||
showAlert(R.string.app_too_old, R.string.must_update)
|
||||
else {
|
||||
|
||||
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
|
||||
if (curVer < MeshService.minFirmwareVersion)
|
||||
showAlert(R.string.firmware_too_old, R.string.firmware_old)
|
||||
if (info != null) {
|
||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
|
||||
if (isOld)
|
||||
showAlert(R.string.app_too_old, R.string.must_update)
|
||||
else {
|
||||
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
|
||||
|
||||
model.radioConfig.value =
|
||||
RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
|
||||
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
|
||||
if (curVer < MeshService.minFirmwareVersion)
|
||||
showAlert(R.string.firmware_too_old, R.string.firmware_old)
|
||||
else {
|
||||
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
|
||||
|
||||
model.channels.value =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
|
||||
model.radioConfig.value =
|
||||
RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig)
|
||||
|
||||
updateNodesFromDevice()
|
||||
model.channels.value =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))
|
||||
|
||||
// we have a connection to our device now, do the channel change
|
||||
perhapsChangeChannel()
|
||||
updateNodesFromDevice()
|
||||
|
||||
// we have a connection to our device now, do the channel change
|
||||
perhapsChangeChannel()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: RemoteException) {
|
||||
|
@ -972,12 +974,11 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
|
||||
try {
|
||||
bindMeshService()
|
||||
}
|
||||
catch(ex: BindFailedException) {
|
||||
} catch (ex: BindFailedException) {
|
||||
// App is probably shutting down, ignore
|
||||
errormsg("Bind of MeshService failed")
|
||||
}
|
||||
|
||||
|
||||
val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
|
||||
if (!bonded && usbDevice == null) // we will handle USB later
|
||||
showSettingsPage()
|
||||
|
|
|
@ -17,10 +17,15 @@ data class Channel(
|
|||
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf
|
||||
)
|
||||
|
||||
private val cleartextPSK = ByteString.EMPTY
|
||||
private val defaultPSK = byteArrayOfInts(1) // a shortstring code to indicate we need our default PSK
|
||||
|
||||
// TH=he unsecured channel that devices ship with
|
||||
val defaultChannel = Channel(
|
||||
ChannelProtos.ChannelSettings.newBuilder()
|
||||
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
|
||||
.setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096)
|
||||
.setPsk(ByteString.copyFrom(defaultPSK))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -50,7 +55,7 @@ data class Channel(
|
|||
val pskIndex = settings.psk.byteAt(0).toInt()
|
||||
|
||||
if (pskIndex == 0)
|
||||
ByteString.EMPTY // Treat as an empty PSK (no encryption)
|
||||
cleartextPSK
|
||||
else {
|
||||
// Treat an index of 1 as the old channelDefaultKey and work up from there
|
||||
val bytes = channelDefaultKey.clone()
|
||||
|
|
|
@ -134,15 +134,10 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
|
|||
}
|
||||
}
|
||||
|
||||
var region: RadioConfigProtos.RegionCode?
|
||||
get() = radioConfig.value?.preferences?.region
|
||||
var region: RadioConfigProtos.RegionCode
|
||||
get() = meshService?.region?.let { RadioConfigProtos.RegionCode.forNumber(it) } ?: RadioConfigProtos.RegionCode.Unset
|
||||
set(value) {
|
||||
val config = radioConfig.value
|
||||
if (value != null && config != null) {
|
||||
val builder = config.toBuilder()
|
||||
builder.preferencesBuilder.region = value
|
||||
setRadioConfig(builder.build())
|
||||
}
|
||||
meshService?.region = value.number
|
||||
}
|
||||
|
||||
/// hardware info about our local device (can be null)
|
||||
|
|
|
@ -5,4 +5,7 @@ import java.util.*
|
|||
|
||||
open class BLEException(msg: String) : IOException(msg)
|
||||
|
||||
open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid")
|
||||
open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid")
|
||||
|
||||
/// Our interface is being shut down
|
||||
open class BLEConnectionClosing() : BLEException("Connection closing ")
|
|
@ -313,7 +313,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
var fromNumChanged = false
|
||||
|
||||
private fun startWatchingFromNum() {
|
||||
safe!!.setNotify(fromNum, true) {
|
||||
safe?.setNotify(fromNum, true) {
|
||||
// We might get multiple notifies before we get around to reading from the radio - so just set one flag
|
||||
fromNumChanged = true
|
||||
debug("fromNum changed")
|
||||
|
@ -469,7 +469,12 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
safe =
|
||||
null // We do this first, because if we throw we still want to mark that we no longer have a valid connection
|
||||
|
||||
s?.close()
|
||||
try {
|
||||
s?.close()
|
||||
}
|
||||
catch(_: BLEConnectionClosing) {
|
||||
warn("Ignoring BLE errors while closing")
|
||||
}
|
||||
} else {
|
||||
debug("Radio was not connected, skipping disable")
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class MeshService : Service(), Logging {
|
|||
/* @Deprecated(message = "Does not filter by port number. For legacy reasons only broadcast for UNKNOWN_APP, switch to ACTION_RECEIVED")
|
||||
const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA" */
|
||||
|
||||
fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
||||
private fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum"
|
||||
|
||||
/// generate a RECEIVED action filter string that includes either the portnumber as an int, or preferably a symbolic name from portnums.proto
|
||||
fun actionReceived(portNum: Int): String {
|
||||
|
@ -549,7 +549,7 @@ class MeshService : Service(), Logging {
|
|||
setChannel(it)
|
||||
}
|
||||
|
||||
channels = asChannels.toTypedArray()
|
||||
channels = fixupChannelList(asChannels).toTypedArray()
|
||||
}
|
||||
|
||||
/// Generate a new mesh packet builder with our node as the sender, and the specified node num
|
||||
|
@ -946,28 +946,22 @@ class MeshService : Service(), Logging {
|
|||
/// If we just changed our nodedb, we might want to do somethings
|
||||
private fun onNodeDBChanged() {
|
||||
maybeUpdateServiceStatusNotification()
|
||||
|
||||
serviceScope.handledLaunch(Dispatchers.Main) {
|
||||
setupLocationRequest()
|
||||
}
|
||||
}
|
||||
|
||||
private var locationRequestInterval: Long = 0;
|
||||
private fun setupLocationRequest() {
|
||||
val desiredInterval: Long = if (myNodeInfo?.hasGPS == true) {
|
||||
0L // no requests when device has GPS
|
||||
} else if (numOnlineNodes < 2) {
|
||||
5 * 60 * 1000L // send infrequently, device needs these requests to set its clock
|
||||
var desiredInterval = 0L
|
||||
|
||||
if (myNodeInfo?.hasGPS == true)
|
||||
desiredInterval =
|
||||
radioConfig?.preferences?.positionBroadcastSecs?.times(1000L) ?: 5 * 60 * 1000L
|
||||
|
||||
stopLocationRequests()
|
||||
if (desiredInterval != 0L) {
|
||||
debug("desired GPS assistance interval $desiredInterval")
|
||||
startLocationRequests(desiredInterval)
|
||||
} else {
|
||||
radioConfig?.preferences?.positionBroadcastSecs?.times(1000L) ?: 5 * 60 * 1000L
|
||||
}
|
||||
|
||||
debug("desired location request $desiredInterval, current $locationRequestInterval")
|
||||
|
||||
if (desiredInterval != locationRequestInterval) {
|
||||
if (locationRequestInterval > 0) stopLocationRequests()
|
||||
if (desiredInterval > 0) startLocationRequests(desiredInterval)
|
||||
locationRequestInterval = desiredInterval
|
||||
debug("No GPS assistance desired, but sending UTC time to mesh")
|
||||
sendPositionScoped()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1315,13 +1309,44 @@ class MeshService : Service(), Logging {
|
|||
radioConfig = null
|
||||
|
||||
// prefill the channel array with null channels
|
||||
channels = Array(myInfo.maxChannels) {
|
||||
val b = ChannelProtos.Channel.newBuilder()
|
||||
b.index = it
|
||||
b.build()
|
||||
}
|
||||
channels = fixupChannelList(listOf<ChannelProtos.Channel>()).toTypedArray()
|
||||
}
|
||||
|
||||
/// scan the channel list and make sure it has one PRIMARY channel and is maxChannels long
|
||||
private fun fixupChannelList(lIn: List<ChannelProtos.Channel>): List<ChannelProtos.Channel> {
|
||||
val mi = myNodeInfo
|
||||
var l = lIn
|
||||
if (mi != null)
|
||||
while (l.size < mi.maxChannels) {
|
||||
val b = ChannelProtos.Channel.newBuilder()
|
||||
b.index = l.size
|
||||
l += b.build()
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
|
||||
private fun setRegionOnDevice() {
|
||||
val curConfigRegion =
|
||||
radioConfig?.preferences?.region ?: RadioConfigProtos.RegionCode.Unset
|
||||
|
||||
if (curConfigRegion.number != curRegionValue && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE)
|
||||
if (deviceVersion >= minFirmwareVersion) {
|
||||
info("Telling device to upgrade region")
|
||||
|
||||
// Tell the device to set the new region field (old devices will simply ignore this)
|
||||
radioConfig?.let { currentConfig ->
|
||||
val newConfig = currentConfig.toBuilder()
|
||||
|
||||
val newPrefs = currentConfig.preferences.toBuilder()
|
||||
newPrefs.regionValue = curRegionValue
|
||||
newConfig.preferences = newPrefs.build()
|
||||
|
||||
sendRadioConfig(newConfig.build())
|
||||
}
|
||||
} else
|
||||
warn("Device is too old to understand region changes")
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are updating nodes we might need to use old (fixed by firmware build)
|
||||
|
@ -1356,24 +1381,7 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
// If nothing was set in our (new style radio preferences, but we now have a valid setting - slam it in)
|
||||
if (curConfigRegion == RadioConfigProtos.RegionCode.Unset && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE) {
|
||||
if (deviceVersion >= minFirmwareVersion) {
|
||||
info("Telling device to upgrade region")
|
||||
|
||||
// Tell the device to set the new region field (old devices will simply ignore this)
|
||||
radioConfig?.let { currentConfig ->
|
||||
val newConfig = currentConfig.toBuilder()
|
||||
|
||||
val newPrefs = currentConfig.preferences.toBuilder()
|
||||
newPrefs.regionValue = curRegionValue
|
||||
newConfig.preferences = newPrefs.build()
|
||||
|
||||
sendRadioConfig(newConfig.build())
|
||||
}
|
||||
}
|
||||
else
|
||||
warn("Device is too old to understand region changes")
|
||||
}
|
||||
setRegionOnDevice()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1388,6 +1396,8 @@ class MeshService : Service(), Logging {
|
|||
reportConnection()
|
||||
|
||||
updateRegion()
|
||||
|
||||
setupLocationRequest() // start sending location packets if needed
|
||||
}
|
||||
|
||||
private fun handleConfigComplete(configCompleteId: Int) {
|
||||
|
@ -1466,13 +1476,13 @@ class MeshService : Service(), Logging {
|
|||
* Must be called from serviceScope. Use sendPositionScoped() for direct calls.
|
||||
*/
|
||||
private fun sendPosition(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
alt: Int,
|
||||
lat: Double = 0.0,
|
||||
lon: Double = 0.0,
|
||||
alt: Int = 0,
|
||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) {
|
||||
debug("Sending our position to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
||||
debug("Sending our position/time to=$destNum lat=$lat, lon=$lon, alt=$alt")
|
||||
|
||||
val position = MeshProtos.Position.newBuilder().also {
|
||||
it.longitudeI = Position.degI(lon)
|
||||
|
@ -1499,15 +1509,15 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
private fun sendPositionScoped(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
alt: Int,
|
||||
lat: Double = 0.0,
|
||||
lon: Double = 0.0,
|
||||
alt: Int = 0,
|
||||
destNum: Int = DataPacket.NODENUM_BROADCAST,
|
||||
wantResponse: Boolean = false
|
||||
) = serviceScope.handledLaunch {
|
||||
try {
|
||||
sendPosition(lat, lon, alt, destNum, wantResponse)
|
||||
} catch (ex: RadioNotConnectedException) {
|
||||
} catch (ex: BLEException) {
|
||||
warn("Ignoring disconnected radio during gps location update")
|
||||
}
|
||||
}
|
||||
|
@ -1627,8 +1637,10 @@ class MeshService : Service(), Logging {
|
|||
} else {
|
||||
debug("Creating firmware update coroutine")
|
||||
updateJob = serviceScope.handledLaunch {
|
||||
debug("Starting firmware update coroutine")
|
||||
SoftwareUpdateService.doUpdate(this@MeshService, safe, filename)
|
||||
exceptionReporter {
|
||||
debug("Starting firmware update coroutine")
|
||||
SoftwareUpdateService.doUpdate(this@MeshService, safe, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1660,7 +1672,7 @@ class MeshService : Service(), Logging {
|
|||
offlineSentPackets.add(p)
|
||||
}
|
||||
|
||||
val binder = object : IMeshService.Stub() {
|
||||
private val binder = object : IMeshService.Stub() {
|
||||
|
||||
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
|
||||
debug("Passing through device change to radio service: ${deviceAddr.anonymize}")
|
||||
|
@ -1684,6 +1696,12 @@ class MeshService : Service(), Logging {
|
|||
}
|
||||
|
||||
override fun getUpdateStatus(): Int = SoftwareUpdateService.progress
|
||||
override fun getRegion(): Int = curRegionValue
|
||||
|
||||
override fun setRegion(regionCode: Int) = toRemoteExceptions {
|
||||
curRegionValue = regionCode
|
||||
setRegionOnDevice()
|
||||
}
|
||||
|
||||
override fun startFirmwareUpdate() = toRemoteExceptions {
|
||||
doFirmwareUpdate()
|
||||
|
|
|
@ -325,7 +325,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
|||
if (newWork.timeoutMillis != 0L) {
|
||||
|
||||
activeTimeout = serviceScope.launch {
|
||||
debug("Starting failsafe timer ${newWork.timeoutMillis}")
|
||||
// debug("Starting failsafe timer ${newWork.timeoutMillis}")
|
||||
delay(newWork.timeoutMillis)
|
||||
errormsg("Failsafe BLE timer expired!")
|
||||
completeWork(
|
||||
|
@ -415,7 +415,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
|||
if (work == null)
|
||||
warn("wor completed, but we already killed it via failsafetimer? status=$status, res=$res")
|
||||
else {
|
||||
debug("work ${work.tag} is completed, resuming status=$status, res=$res")
|
||||
// debug("work ${work.tag} is completed, resuming status=$status, res=$res")
|
||||
if (status != 0)
|
||||
work.completion.resumeWithException(
|
||||
BLEStatusException(
|
||||
|
@ -773,7 +773,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
|
|||
|
||||
closeGatt()
|
||||
|
||||
failAllWork(BLEException("Connection closing"))
|
||||
failAllWork(BLEConnectionClosing())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -374,13 +374,17 @@ class SoftwareUpdateService : JobIntentService(), Logging {
|
|||
throw DeviceRejectedException()
|
||||
|
||||
// Send all the blocks
|
||||
var oldProgress = -1 // used to limit # of log spam
|
||||
while (firmwareNumSent < firmwareSize) {
|
||||
// If we are doing the spiffs partition, we limit progress to a max of 50%, so that the user doesn't think we are done
|
||||
// yet
|
||||
val maxProgress = if(flashRegion != FLASH_REGION_APPLOAD)
|
||||
50 else 100
|
||||
sendProgress(context, firmwareNumSent * maxProgress / firmwareSize, isAppload)
|
||||
debug("sending block ${progress}%")
|
||||
if(progress != oldProgress) {
|
||||
debug("sending block ${progress}%")
|
||||
oldProgress = progress;
|
||||
}
|
||||
var blockSize = 512 - 3 // Max size MTU excluding framing
|
||||
|
||||
if (blockSize > firmwareStream.available())
|
||||
|
|
|
@ -18,7 +18,6 @@ import com.geeksville.android.Logging
|
|||
import com.geeksville.android.hideKeyboard
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.ChannelProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.databinding.ChannelFragmentBinding
|
||||
import com.geeksville.mesh.model.Channel
|
||||
|
@ -50,6 +49,7 @@ fun ImageView.setOpaque() {
|
|||
class ChannelFragment : ScreenFragment("Channel"), Logging {
|
||||
|
||||
private var _binding: ChannelFragmentBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
|
@ -81,6 +81,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
val channels = model.channels.value
|
||||
val channel = channels?.primaryChannel
|
||||
|
||||
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
||||
|
||||
// Only let buttons work if we are connected to the radio
|
||||
binding.shareButton.isEnabled = connected
|
||||
binding.resetButton.isEnabled = connected
|
||||
|
||||
binding.editableCheckbox.isChecked = false // start locked
|
||||
if (channel != null) {
|
||||
binding.qrView.visibility = View.VISIBLE
|
||||
|
@ -89,7 +95,6 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
|
||||
// For now, we only let the user edit/save channels while the radio is awake - because the service
|
||||
// doesn't cache radioconfig writes.
|
||||
val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED
|
||||
binding.editableCheckbox.isEnabled = connected
|
||||
|
||||
binding.qrView.setImageBitmap(channels.getChannelQR())
|
||||
|
@ -143,6 +148,28 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
/// Send new channel settings to the device
|
||||
private fun installSettings(newChannel: ChannelProtos.ChannelSettings) {
|
||||
val newSet =
|
||||
ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
|
||||
// Try to change the radio, if it fails, tell the user why and throw away their redits
|
||||
try {
|
||||
model.setChannels(newSet)
|
||||
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("ignoring channel problem", ex)
|
||||
|
||||
setGUIfromModel() // Throw away user edits
|
||||
|
||||
// Tell the user to try again
|
||||
Snackbar.make(
|
||||
binding.editableCheckbox,
|
||||
R.string.radio_sleeping,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
@ -150,6 +177,21 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
requireActivity().hideKeyboard()
|
||||
}
|
||||
|
||||
binding.resetButton.setOnClickListener { _ ->
|
||||
// User just locked it, we should warn and then apply changes to radio
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.reset_to_defaults)
|
||||
.setMessage(R.string.are_you_shure_change_default)
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
setGUIfromModel() // throw away any edits
|
||||
}
|
||||
.setPositiveButton(getString(R.string.accept)) { _, _ ->
|
||||
debug("Switching back to default channel")
|
||||
installSettings(Channel.defaultChannel.settings)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
|
||||
binding.editableCheckbox.setOnClickListener { _ ->
|
||||
val checked = binding.editableCheckbox.isChecked
|
||||
|
@ -178,41 +220,26 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|||
ignoreCase = true
|
||||
)
|
||||
) {
|
||||
// Install a new customized channel
|
||||
|
||||
debug("ASSIGNING NEW AES256 KEY")
|
||||
val random = SecureRandom()
|
||||
val bytes = ByteArray(32)
|
||||
random.nextBytes(bytes)
|
||||
newSettings.psk = ByteString.copyFrom(bytes)
|
||||
|
||||
val selectedChannelOptionString =
|
||||
binding.filledExposedDropdown.editableText.toString()
|
||||
val modemConfig = getModemConfig(selectedChannelOptionString)
|
||||
|
||||
if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
|
||||
newSettings.modemConfig = modemConfig
|
||||
} else {
|
||||
debug("Switching back to default channel")
|
||||
newSettings = Channel.defaultChannel.settings.toBuilder()
|
||||
}
|
||||
|
||||
val selectedChannelOptionString =
|
||||
binding.filledExposedDropdown.editableText.toString()
|
||||
val modemConfig = getModemConfig(selectedChannelOptionString)
|
||||
|
||||
if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
|
||||
newSettings.modemConfig = modemConfig
|
||||
|
||||
val newChannel = newSettings.build()
|
||||
val newSet = ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build())
|
||||
// Try to change the radio, if it fails, tell the user why and throw away their redits
|
||||
try {
|
||||
model.setChannels(newSet)
|
||||
// Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc)
|
||||
} catch (ex: RemoteException) {
|
||||
errormsg("ignoring channel problem", ex)
|
||||
|
||||
setGUIfromModel() // Throw away user edits
|
||||
|
||||
// Tell the user to try again
|
||||
Snackbar.make(
|
||||
binding.editableCheckbox,
|
||||
R.string.radio_sleeping,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
installSettings(newSettings.build())
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
|
|
@ -524,7 +524,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
}
|
||||
|
||||
/// Set the correct update button configuration based on current progress
|
||||
private fun refreshUpdateButton() {
|
||||
private fun refreshUpdateButton(enable: Boolean) {
|
||||
debug("Reiniting the udpate button")
|
||||
val info = model.myNodeInfo.value
|
||||
val service = model.meshService
|
||||
|
@ -535,7 +535,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
val progress = service.updateStatus
|
||||
|
||||
binding.updateFirmwareButton.isEnabled =
|
||||
binding.updateFirmwareButton.isEnabled = enable &&
|
||||
(progress < 0) // if currently doing an upgrade disable button
|
||||
|
||||
if (progress >= 0) {
|
||||
|
@ -572,7 +572,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
val connected = model.isConnected.value
|
||||
|
||||
val isConnected = connected == MeshService.ConnectionState.CONNECTED
|
||||
binding.nodeSettings.visibility = if(isConnected) View.VISIBLE else View.GONE
|
||||
binding.nodeSettings.visibility = if (isConnected) View.VISIBLE else View.GONE
|
||||
|
||||
if (connected == MeshService.ConnectionState.DISCONNECTED)
|
||||
model.ownerName.value = ""
|
||||
|
@ -582,25 +582,19 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
val spinner = binding.regionSpinner
|
||||
val unsetIndex = regions.indexOf(RadioConfigProtos.RegionCode.Unset.name)
|
||||
spinner.onItemSelectedListener = null
|
||||
if(region != null) {
|
||||
debug("current region is $region")
|
||||
var regionIndex = regions.indexOf(region.name)
|
||||
if(regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
|
||||
regionIndex = unsetIndex
|
||||
|
||||
// We don't want to be notified of our own changes, so turn off listener while making them
|
||||
spinner.setSelection(regionIndex, false)
|
||||
spinner.onItemSelectedListener = regionSpinnerListener
|
||||
spinner.isEnabled = true
|
||||
}
|
||||
else {
|
||||
warn("region is unset!")
|
||||
spinner.setSelection(unsetIndex, false)
|
||||
spinner.isEnabled = false // leave disabled, because we can't get our region
|
||||
}
|
||||
debug("current region is $region")
|
||||
var regionIndex = regions.indexOf(region.name)
|
||||
if (regionIndex == -1) // Not found, probably because the device has a region our app doesn't yet understand. Punt and say Unset
|
||||
regionIndex = unsetIndex
|
||||
|
||||
// We don't want to be notified of our own changes, so turn off listener while making them
|
||||
spinner.setSelection(regionIndex, false)
|
||||
spinner.onItemSelectedListener = regionSpinnerListener
|
||||
spinner.isEnabled = true
|
||||
|
||||
// If actively connected possibly let the user update firmware
|
||||
refreshUpdateButton()
|
||||
refreshUpdateButton(region != RadioConfigProtos.RegionCode.Unset)
|
||||
|
||||
// Update the status string (highest priority messages first)
|
||||
val info = model.myNodeInfo.value
|
||||
|
@ -620,7 +614,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener{
|
||||
private val regionSpinnerListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>,
|
||||
view: View,
|
||||
|
@ -652,7 +646,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
// init our region spinner
|
||||
val spinner = binding.regionSpinner
|
||||
val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
||||
val regionAdapter =
|
||||
ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions)
|
||||
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = regionAdapter
|
||||
|
||||
|
@ -965,7 +960,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
private val updateProgressReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
refreshUpdateButton()
|
||||
refreshUpdateButton(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14.99,4.59V5c0,1.1 -0.9,2 -2,2h-2v2c0,0.55 -0.45,1 -1,1h-2v2h6c0.55,0 1,0.45 1,1v3h1c0.89,0 1.64,0.59 1.9,1.4C19.19,15.98 20,14.08 20,12c0,-3.35 -2.08,-6.23 -5.01,-7.41zM8.99,16v-1l-4.78,-4.78C4.08,10.79 4,11.39 4,12c0,4.07 3.06,7.43 6.99,7.93V18c-1.1,0 -2,-0.9 -2,-2z"
|
||||
android:strokeAlpha="0.3"
|
||||
android:fillAlpha="0.3"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10.99,19.93C7.06,19.43 4,16.07 4,12c0,-0.61 0.08,-1.21 0.21,-1.78L8.99,15v1c0,1.1 0.9,2 2,2v1.93zM17.89,17.4c-0.26,-0.81 -1,-1.4 -1.9,-1.4h-1v-3c0,-0.55 -0.45,-1 -1,-1h-6v-2h2c0.55,0 1,-0.45 1,-1L10.99,7h2c1.1,0 2,-0.9 2,-2v-0.41C17.92,5.77 20,8.65 20,12c0,2.08 -0.81,3.98 -2.11,5.4z"/>
|
||||
</vector>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -91,30 +92,46 @@
|
|||
<AutoCompleteTextView
|
||||
android:id="@+id/filled_exposed_dropdown"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/set_channel_options" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/resetButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/reset"
|
||||
app:icon="@drawable/ic_twotone_public_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/editableCheckbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="96dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:button="@drawable/sl_lock_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/shareButton"
|
||||
app:layout_constraintStart_toStartOf="parent"></CheckBox>
|
||||
app:layout_constraintBottom_toBottomOf="@id/bottomButtonsGuideline"
|
||||
app:layout_constraintStart_toEndOf="@+id/resetButton"></CheckBox>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/shareButton"
|
||||
style="@android:style/Widget.Material.ImageButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="96dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:contentDescription="@string/share"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/editableCheckbox"
|
||||
app:srcCompat="@drawable/ic_twotone_share_24" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/bottomButtonsGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_end="16dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -97,4 +97,8 @@
|
|||
<string name="cant_change_no_radio">Couldn\'t change channel, because radio is not yet connected. Please try again.</string>
|
||||
<string name="sample_coords">55.332244 34.442211</string>
|
||||
<string name="save_messages">Save messages as csv...</string>
|
||||
<string name="set_channel_options">Set channel options</string>
|
||||
<string name="reset">Reset</string>
|
||||
<string name="are_you_shure_change_default">Are you sure you want to change to the default channel?</string>
|
||||
<string name="reset_to_defaults">Reset to defaults</string>
|
||||
</resources>
|
||||
|
|
Ładowanie…
Reference in New Issue