diff --git a/app/build.gradle b/app/build.gradle index 5cac80212..4704ea736 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index a9ee757d0..17382d016 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -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); } diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index c7879cf1d..bfceab118 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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() diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt index 665a8de25..06855b66a 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -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() diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index fe4175e9b..6b79ba688 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -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) diff --git a/app/src/main/java/com/geeksville/mesh/service/BLEException.kt b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt index 4ee38cd1b..7aa2724bf 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BLEException.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt @@ -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") \ No newline at end of file +open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid") + +/// Our interface is being shut down +open class BLEConnectionClosing() : BLEException("Connection closing ") \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt index a424f8640..1fbfdcad2 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt @@ -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") } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 68e9e1652..9a0d41a98 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -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()).toTypedArray() } + /// scan the channel list and make sure it has one PRIMARY channel and is maxChannels long + private fun fixupChannelList(lIn: List): List { + 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() diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index ee460c9ca..c77830656 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -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()) } /** diff --git a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt index b9050105e..663070f85 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt @@ -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()) diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index 9ca623088..a8f5457b1 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -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() diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index fde6466bd..11d552ab3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -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) } } diff --git a/app/src/main/res/drawable/ic_twotone_public_24.xml b/app/src/main/res/drawable/ic_twotone_public_24.xml new file mode 100644 index 000000000..403051e61 --- /dev/null +++ b/app/src/main/res/drawable/ic_twotone_public_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/layout/channel_fragment.xml b/app/src/main/res/layout/channel_fragment.xml index 461eafab0..597bcb4bf 100644 --- a/app/src/main/res/layout/channel_fragment.xml +++ b/app/src/main/res/layout/channel_fragment.xml @@ -1,6 +1,7 @@ @@ -91,30 +92,46 @@ + android:layout_height="wrap_content" + android:hint="@string/set_channel_options" /> +