diff --git a/app/build.gradle b/app/build.gradle
index 9e3aa28ca..dfe11bfc4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.github.triplet.play'
@@ -25,13 +25,13 @@ android {
}
} */
compileSdkVersion 29
- buildToolsVersion "30.0.1" // Note: 30.0.0.2 doesn't yet work on Github actions CI
+ buildToolsVersion "30.0.2" // Note: 30.0.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 20108 // format is Mmmss (where M is 1+the numeric major number
- versionName "1.1.8"
+ versionCode 20120 // format is Mmmss (where M is 1+the numeric major number
+ versionName "1.1.20"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -59,6 +59,8 @@ android {
buildFeatures {
// Enables Jetpack Compose for this module
// compose true // NOTE, if true main app crashes if you use regular view layout functions
+
+ viewBinding true
}
// Set both the Java and Kotlin compilers to target Java 8.
@@ -85,10 +87,6 @@ play {
serviceAccountCredentials = file("../../play-credentials.json")
}
-androidExtensions {
- experimental = true
-}
-
// per protobuf-gradle-plugin docs, this is recommended for android
protobuf {
protoc {
@@ -147,7 +145,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.13.0')
+ implementation ('com.google.protobuf:protobuf-java:3.14.0')
// For UART access
// implementation 'com.google.android.things:androidthings:1.0'
@@ -166,7 +164,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.2.2'
+ implementation 'com.google.firebase:firebase-crashlytics:17.3.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"
@@ -177,7 +175,7 @@ dependencies {
// barcode support
// per https://github.com/journeyapps/zxing-android-embedded for support of android version 22
implementation('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
- implementation 'com.google.zxing:core:3.4.0'
+ implementation 'com.google.zxing:core:3.4.1'
def work_version = '2.4.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5422c73bc..69e08be69 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -133,7 +133,7 @@
diff --git a/app/src/main/java/com/geeksville/mesh/DataPacket.kt b/app/src/main/java/com/geeksville/mesh/DataPacket.kt
index df8e3adfc..1b23b7d4c 100644
--- a/app/src/main/java/com/geeksville/mesh/DataPacket.kt
+++ b/app/src/main/java/com/geeksville/mesh/DataPacket.kt
@@ -2,7 +2,7 @@ package com.geeksville.mesh
import android.os.Parcel
import android.os.Parcelable
-import kotlinx.android.parcel.Parcelize
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@@ -22,7 +22,7 @@ enum class MessageStatus : Parcelable {
data class DataPacket(
var to: String? = ID_BROADCAST, // a nodeID string, or ID_BROADCAST for broadcast
val bytes: ByteArray?,
- val dataType: Int, // A value such as MeshProtos.Data.Type.OPAQUE_VALUE
+ val dataType: Int, // A port number for this packet (formerly called DataType, see portnums.proto for new usage instructions)
var from: String? = ID_LOCAL, // a nodeID string, or ID_LOCAL for localhost
var time: Long = System.currentTimeMillis(), // msecs since 1970
var id: Int = 0, // 0 means unassigned
@@ -39,14 +39,14 @@ data class DataPacket(
*/
constructor(to: String? = ID_BROADCAST, text: String) : this(
to, text.toByteArray(utf8),
- MeshProtos.Data.Type.CLEAR_TEXT_VALUE
+ Portnums.PortNum.TEXT_MESSAGE_APP_VALUE
)
/**
* If this is a text message, return the string, otherwise null
*/
val text: String?
- get() = if (dataType == MeshProtos.Data.Type.CLEAR_TEXT_VALUE)
+ get() = if (dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
bytes?.toString(utf8)
else
null
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index ff77040c4..52a5152c1 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -38,6 +38,7 @@ import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch
+import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.*
@@ -54,7 +55,6 @@ import com.google.android.material.tabs.TabLayoutMediator
import com.google.protobuf.InvalidProtocolBufferException
import com.vorlonsoft.android.rate.AppRate
import com.vorlonsoft.android.rate.StoreType
-import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -124,6 +124,8 @@ class MainActivity : AppCompatActivity(), Logging,
13 // seems to be hardwired in CompanionDeviceManager to add 65536
}
+ private lateinit var binding: ActivityMainBinding
+
// Used to schedule a coroutine in the GUI thread
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
@@ -324,14 +326,14 @@ class MainActivity : AppCompatActivity(), Logging,
DataPacket(
"+16508675310",
testPayload,
- MeshProtos.Data.Type.OPAQUE_VALUE
+ Portnums.PortNum.PRIVATE_APP_VALUE
)
)
m.send(
DataPacket(
"+16508675310",
testPayload,
- MeshProtos.Data.Type.CLEAR_TEXT_VALUE
+ Portnums.PortNum.TEXT_MESSAGE_APP_VALUE
)
)
}
@@ -366,6 +368,8 @@ class MainActivity : AppCompatActivity(), Logging,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+
val prefs = UIViewModel.getPreferences(this)
model.ownerName.value = prefs.getString("owner", "")!!
@@ -396,15 +400,15 @@ class MainActivity : AppCompatActivity(), Logging,
/* setContent {
MeshApp()
} */
- setContentView(R.layout.activity_main)
+ setContentView(binding.root)
initToolbar()
- pager.adapter = tabsAdapter
- pager.isUserInputEnabled =
+ binding.pager.adapter = tabsAdapter
+ binding.pager.isUserInputEnabled =
false // Gestures for screen switching doesn't work so good with the map view
// pager.offscreenPageLimit = 0 // Don't keep any offscreen pages around, because we want to make sure our bluetooth scanning stops
- TabLayoutMediator(tab_layout, pager) { tab, position ->
+ TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position ->
// tab.text = tabInfos[position].text // I think it looks better with icons only
tab.icon = getDrawable(tabInfos[position].icon)
}.attach()
@@ -707,7 +711,7 @@ class MainActivity : AppCompatActivity(), Logging,
intent.getParcelableExtra(EXTRA_PAYLOAD)!!
when (payload.dataType) {
- MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
+ Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> {
model.messagesState.addMessage(payload)
}
else ->
@@ -893,7 +897,7 @@ class MainActivity : AppCompatActivity(), Logging,
}
private fun showSettingsPage() {
- pager.currentItem = 5
+ binding.pager.currentItem = 5
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
index f0a81085a..77d26af77 100644
--- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
+++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt
@@ -4,7 +4,7 @@ import android.os.Parcelable
import com.geeksville.mesh.ui.bearing
import com.geeksville.mesh.ui.latLongToMeter
import com.geeksville.util.anonymize
-import kotlinx.android.parcel.Parcelize
+import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
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 7cd889554..e698dbccb 100644
--- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
@@ -31,17 +31,17 @@ data class Channel(
MeshProtos.ChannelSettings.newBuilder().setName(defaultChannelName)
.setModemConfig(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build()
)
-
- private const val prefixRoot = "https://www.meshtastic.org/c/"
- const val prefix = "$prefixRoot#"
+
+ const val prefix = "https://www.meshtastic.org/c/#"
private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP
private fun urlToSettings(url: Uri): MeshProtos.ChannelSettings {
val urlStr = url.toString()
- // Let the path optionally include the # character (or not) so we can work with old URLs generated by old versions of the app
- val pathRegex = Regex("$prefixRoot#?(.*)")
+ // We no longer support the super old (about 0.8ish? verison of the URLs that don't use the # separator - I doubt
+ // anyone is still using that old format
+ val pathRegex = Regex("$prefix(.*)")
val (base64) = pathRegex.find(urlStr)?.destructured
?: throw MalformedURLException("Not a meshtastic URL: ${urlStr.take(40)}")
val bytes = Base64.decode(base64, base64Flags)
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 657aaf963..4ee38cd1b 100644
--- a/app/src/main/java/com/geeksville/mesh/service/BLEException.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/BLEException.kt
@@ -1,5 +1,8 @@
package com.geeksville.mesh.service
import java.io.IOException
+import java.util.*
-open class BLEException(msg: String) : IOException(msg)
\ No newline at end of file
+open class BLEException(msg: String) : IOException(msg)
+
+open class BLECharacteristicNotFoundException(uuid: UUID) : BLEException("Can't get characteristic $uuid")
\ 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 5f4d2bb45..a1e1eda66 100644
--- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt
@@ -489,6 +489,6 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
* Get a chracteristic, but in a safe manner because some buggy BLE implementations might return null
*/
private fun getCharacteristic(uuid: UUID) =
- bservice.getCharacteristic(uuid) ?: throw BLEException("Can't get characteristic $uuid")
+ bservice.getCharacteristic(uuid) ?: throw BLECharacteristicNotFoundException(uuid)
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt b/app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt
new file mode 100644
index 000000000..13ef461e0
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt
@@ -0,0 +1,33 @@
+package com.geeksville.mesh.service
+
+import com.geeksville.android.Logging
+
+/**
+ * Provide structured access to parse and compare device version strings
+ */
+data class DeviceVersion(val asString: String): Comparable, Logging {
+
+ val asInt get() = try {
+ verStringToInt(asString)
+ } catch(e: Exception) {
+ warn("Exception while parsing version '$asString', assuming version 0")
+ 0
+ }
+
+ /**
+ * Convert a version string of the form 1.23.57 to a comparable integer of
+ * the form 12357.
+ *
+ * Or throw an exception if the string can not be parsed
+ */
+ private fun verStringToInt(s: String): Int {
+ // Allow 1 to two digits per match
+ val match =
+ Regex("(\\d{1,2}).(\\d{1,2}).(\\d{1,2})").find(s)
+ ?: throw Exception("Can't parse version $s")
+ val (major, minor, build) = match.destructured
+ return major.toInt() * 1000 + minor.toInt() * 100 + build.toInt()
+ }
+
+ override fun compareTo(other: DeviceVersion): Int = asInt - other.asInt
+}
\ No newline at end of file
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 2cd528cf3..c294e8ac3 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -91,6 +91,9 @@ class MeshService : Service(), Logging {
private var packetRepo: PacketRepository? = null
private var fusedLocationClient: FusedLocationProviderClient? = null
+ // If we've ever read a valid region code from our device it will be here
+ var curRegionValue = MeshProtos.RegionCode.Unset_VALUE
+
val radio = ServiceClient {
IRadioInterfaceService.Stub.asInterface(it).apply {
// Now that we are connected to the radio service, tell it to connect to the radio
@@ -418,6 +421,8 @@ class MeshService : Service(), Logging {
/// END OF MODEL
///
+ val deviceVersion get() = DeviceVersion(myNodeInfo?.firmwareVersion ?: "")
+
/// Map a nodenum to a node, or throw an exception if not found
private fun toNodeInfo(n: Int) = nodeDBbyNodeNum[n] ?: throw NodeNumNotFoundException(
n
@@ -547,7 +552,7 @@ class MeshService : Service(), Logging {
to = toId,
time = rxTime * 1000L,
id = packet.id,
- dataType = data.typValue,
+ dataType = data.portnumValue,
bytes = bytes
)
}
@@ -555,12 +560,15 @@ class MeshService : Service(), Logging {
}
}
+ /// Syntactic sugar to create data subpackets
+ private fun makeData(portNum: Int, bytes: ByteString) = MeshProtos.Data.newBuilder().also {
+ it.portnumValue = portNum
+ it.payload = bytes
+ }.build()
+
private fun toMeshPacket(p: DataPacket): MeshPacket {
return buildMeshPacket(p.to!!, id = p.id, wantAck = true) {
- data = MeshProtos.Data.newBuilder().also {
- it.typ = MeshProtos.Data.Type.forNumber(p.dataType)
- it.payload = ByteString.copyFrom(p.bytes)
- }.build()
+ data = makeData(p.dataType, ByteString.copyFrom(p.bytes))
}
}
@@ -591,31 +599,36 @@ class MeshService : Service(), Logging {
if (myInfo.myNodeNum == packet.from)
debug("Ignoring retransmission of our packet ${bytes.size}")
else {
- debug("Received data from $fromId ${bytes.size}")
+ debug("Received data from $fromId, portnum=${data.portnumValue} ${bytes.size} bytes")
dataPacket.status = MessageStatus.RECEIVED
rememberDataPacket(dataPacket)
- when (data.typValue) {
- MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
+ when (data.portnumValue) {
+ Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> {
debug("Received CLEAR_TEXT from $fromId")
recentReceivedTextPacket = dataPacket
updateNotification()
- serviceBroadcasts.broadcastReceivedData(dataPacket)
}
- MeshProtos.Data.Type.CLEAR_READACK_VALUE ->
- warn(
- "TODO ignoring CLEAR_READACK from $fromId"
- )
+ // Handle new style position info
+ Portnums.PortNum.POSITION_APP_VALUE -> {
+ val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond()
+ val u = MeshProtos.Position.parseFrom(data.payload)
+ handleReceivedPosition(packet.from, u, rxTime)
+ }
- MeshProtos.Data.Type.OPAQUE_VALUE ->
- serviceBroadcasts.broadcastReceivedData(dataPacket)
-
- else -> TODO()
+ // Handle new style user info
+ Portnums.PortNum.NODEINFO_APP_VALUE -> {
+ val u = MeshProtos.User.parseFrom(data.payload)
+ handleReceivedUser(packet.from, u)
+ }
}
+ // We always tell other apps when new data packets arrive
+ serviceBroadcasts.broadcastReceivedData(dataPacket)
+
GeeksvilleApplication.analytics.track(
"num_data_receive",
DataPair(1)
@@ -624,7 +637,7 @@ class MeshService : Service(), Logging {
GeeksvilleApplication.analytics.track(
"data_receive",
DataPair("num_bytes", bytes.size),
- DataPair("type", data.typValue)
+ DataPair("type", data.portnumValue)
)
}
}
@@ -1061,7 +1074,10 @@ class MeshService : Service(), Logging {
hwModel,
firmwareVersion,
firmwareUpdateFilename != null,
- SoftwareUpdateService.shouldUpdate(this@MeshService, firmwareVersion),
+ SoftwareUpdateService.shouldUpdate(
+ this@MeshService,
+ DeviceVersion(firmwareVersion)
+ ),
currentPacketId.toLong() and 0xffffffffL,
if (nodeNumBits == 0) 8 else nodeNumBits,
if (packetIdBits == 0) 8 else packetIdBits,
@@ -1095,8 +1111,7 @@ class MeshService : Service(), Logging {
}
}
- // If we've ever read a valid region code from our device it will be here
- var curRegionValue = MeshProtos.RegionCode.Unset_VALUE
+
/**
* If we are updating nodes we might need to use old (fixed by firmware build)
@@ -1222,7 +1237,14 @@ class MeshService : Service(), Logging {
val packet = newMeshPacketTo(destNum)
packet.decoded = MeshProtos.SubPacket.newBuilder().also {
- it.position = position
+ val isNewPositionAPI = deviceVersion >= DeviceVersion("1.20.0") // We changed position APIs with this version
+ if (isNewPositionAPI) {
+ // Use the new position as data format
+ it.data = makeData(Portnums.PortNum.POSITION_APP_VALUE, position.toByteString())
+ } else {
+ // Send the old dedicated position subpacket
+ it.position = position
+ }
it.wantResponse = wantResponse
}.build()
@@ -1334,7 +1356,7 @@ class MeshService : Service(), Logging {
return 0 // We don't have mynodeinfo yet, so just let the radio eventually assign an ID
}
- var firmwareUpdateFilename: String? = null
+ var firmwareUpdateFilename: UpdateFilenames? = null
/***
* Return the filename we will install on the device
@@ -1458,6 +1480,11 @@ class MeshService : Service(), Logging {
// Keep a record of datapackets, so GUIs can show proper chat history
rememberDataPacket(p)
+ if (p.bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN.number) {
+ p.status = MessageStatus.ERROR
+ throw RemoteException("Message too long")
+ }
+
if (p.id != 0) { // If we have an ID we can wait for an ack or nak
deleteOldPackets()
sentPackets[p.id] = p
diff --git a/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt b/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt
index 883308445..36b87b40d 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt
@@ -38,7 +38,7 @@ class SimRadio(private val context: Context) {
}.build().toByteArray()
) */
- simInitPackets.forEach { json ->
+ simInitPackets.forEach { _ ->
val fromRadio = MeshProtos.FromRadio.newBuilder().apply {
packet = MeshProtos.MeshPacket.newBuilder().apply {
// jsonParser.merge(json, this)
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 1c955064f..69d57639c 100644
--- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt
@@ -18,7 +18,12 @@ import java.util.zip.CRC32
*/
fun toNetworkByteArray(value: Int, formatType: Int): ByteArray {
- val len: Int = 4 // getTypeLen(formatType)
+ val len = when (formatType) {
+ BluetoothGattCharacteristic.FORMAT_UINT8 -> 1
+ BluetoothGattCharacteristic.FORMAT_UINT32 -> 4
+ else -> TODO()
+ }
+
val mValue = ByteArray(len)
when (formatType) {
@@ -44,6 +49,9 @@ fun toNetworkByteArray(value: Int, formatType: Int): ByteArray {
mValue.get(offset++) = (value shr 16 and 0xFF).toByte()
mValue.get(offset) = (value shr 24 and 0xFF).toByte()
} */
+ BluetoothGattCharacteristic.FORMAT_UINT8 ->
+ mValue[0] = (value and 0xFF).toByte()
+
BluetoothGattCharacteristic.FORMAT_UINT32 -> {
mValue[0] = (value and 0xFF).toByte()
mValue[1] = (value shr 8 and 0xFF).toByte()
@@ -55,6 +63,9 @@ fun toNetworkByteArray(value: Int, formatType: Int): ByteArray {
return mValue
}
+
+data class UpdateFilenames(val appLoad: String?, val spiffs: String?)
+
/**
* typical flow
*
@@ -152,6 +163,8 @@ class SoftwareUpdateService : JobIntentService(), Logging {
UUID.fromString("4826129c-c22a-43a3-b066-ce8f0d5bacc6") // write crc32, write last - writing this will complete the OTA operation, now you can read result
private val SW_UPDATE_RESULT_CHARACTER =
UUID.fromString("5e134862-7411-4424-ac4a-210937432c77") // read|notify result code, readable but will notify when the OTA operation completes
+ private val SW_UPDATE_REGION_CHARACTER =
+ UUID.fromString("5e134862-7411-4424-ac4a-210937432c67") // write - used to set the region we are setting (appload vs spiffs)
private val SW_VERSION_CHARACTER = longBLEUUID("2a28")
private val MANUFACTURE_CHARACTER = longBLEUUID("2a29")
@@ -173,20 +186,7 @@ class SoftwareUpdateService : JobIntentService(), Logging {
)
}
- /**
- * Convert a version string of the form 1.23.57 to a comparable integer of
- * the form 12357.
- *
- * Or throw an exception if the string can not be parsed
- */
- fun verStringToInt(s: String): Int {
- // Allow 1 to two digits per match
- val match =
- Regex("(\\d{1,2}).(\\d{1,2}).(\\d{1,2})").find(s)
- ?: throw Exception("Can't parse version $s")
- val (major, minor, build) = match.destructured
- return major.toInt() * 1000 + minor.toInt() * 100 + build.toInt()
- }
+
/** Return true if we thing the firmwarte shoulde be updated
*
@@ -194,15 +194,11 @@ class SoftwareUpdateService : JobIntentService(), Logging {
*/
fun shouldUpdate(
context: Context,
- swVer: String
+ deviceVersion: DeviceVersion
): Boolean = try {
- val curVer = verStringToInt(context.getString(R.string.cur_firmware_version))
+ val curVer = DeviceVersion(context.getString(R.string.cur_firmware_version))
val minVer =
- verStringToInt("0.7.8") // The oldest device version with a working software update service
-
- // If the user is running a development build we never do an automatic update
- val deviceVersion =
- verStringToInt(if (swVer.isEmpty() || swVer == "unset") "99.99.99" else swVer)
+ DeviceVersion("0.7.8") // The oldest device version with a working software update service
(curVer > deviceVersion) && (deviceVersion >= minVer)
} catch (ex: Exception) {
@@ -210,28 +206,36 @@ class SoftwareUpdateService : JobIntentService(), Logging {
false // If we fail parsing our update info
}
- /** Return the filename this device needs to use as an update (or null if no update needed)
+ /** Return a Pair of apploadfilename, spiffs filename this device needs to use as an update (or null if no update needed)
*/
fun getUpdateFilename(
context: Context,
mfg: String
- ): String? {
- val curver = context.getString(R.string.cur_firmware_version)
-
- val base = "firmware-$mfg-$curver.bin"
+ ): UpdateFilenames {
+ val curVer = context.getString(R.string.cur_firmware_version)
// Check to see if the file exists (some builds might not include update files for size reasons)
val firmwareFiles = context.assets.list("firmware") ?: arrayOf()
- return if (firmwareFiles.contains(base))
- "firmware/$base"
- else
- null
+
+ val appLoad = "firmware-$mfg-$curVer.bin"
+ val spiffs = "spiffs-$curVer.bin"
+
+ return UpdateFilenames(
+ if (firmwareFiles.contains(appLoad))
+ "firmware/$appLoad"
+ else
+ null,
+ if (firmwareFiles.contains(spiffs))
+ "firmware/$spiffs"
+ else
+ null
+ )
}
/** Return the filename this device needs to use as an update (or null if no update needed)
* No longer used, because we get update info inband from our radio API
*/
- fun getUpdateFilename(context: Context, sync: SafeBluetooth): String? {
+ fun getUpdateFilename(context: Context, sync: SafeBluetooth): UpdateFilenames? {
val service = sync.gatt!!.services.find { it.uuid == SW_UPDATE_UUID }!!
//val hwVerDesc = service.getCharacteristic(HW_VERSION_CHARACTER)
@@ -248,19 +252,66 @@ class SoftwareUpdateService : JobIntentService(), Logging {
* A public function so that if you have your own SafeBluetooth connection already open
* you can use it for the software update.
*/
- fun doUpdate(context: Context, sync: SafeBluetooth, assetName: String) {
+ fun doUpdate(context: Context, sync: SafeBluetooth, assets: UpdateFilenames) {
+ // we must attempt spiffs first, because if we update the appload the device will reboot afterwards
+ try {
+ assets.spiffs?.let { doUpdate(context, sync, it, FLASH_REGION_SPIFFS) }
+ }
+ catch(_: BLECharacteristicNotFoundException) {
+ // If we can't update spiffs (because not supported by target), do not fail
+ errormsg("Ignoring failure to update spiffs on old appload")
+ }
+ assets.appLoad?.let { doUpdate(context, sync, it, FLASH_REGION_APPLOAD) }
+ progress = -1 // success
+ }
+
+ // writable region codes in the ESP32 update code
+ private val FLASH_REGION_APPLOAD = 0
+ private val FLASH_REGION_SPIFFS = 100
+
+ /**
+ * A public function so that if you have your own SafeBluetooth connection already open
+ * you can use it for the software update.
+ */
+ private fun doUpdate(context: Context, sync: SafeBluetooth, assetName: String, flashRegion: Int = FLASH_REGION_APPLOAD) {
try {
val g = sync.gatt!!
val service = g.services.find { it.uuid == SW_UPDATE_UUID }
?: throw BLEException("Couldn't find update service")
- info("Starting firmware update for $assetName")
+ /**
+ * Get a chracteristic, but in a safe manner because some buggy BLE implementations might return null
+ */
+ fun getCharacteristic(uuid: UUID) =
+ service.getCharacteristic(uuid)
+ ?: throw BLECharacteristicNotFoundException(uuid)
+
+ info("Starting firmware update for $assetName, flash region $flashRegion")
progress = 0
- val totalSizeDesc = service.getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)
- val dataDesc = service.getCharacteristic(SW_UPDATE_DATA_CHARACTER)
- val crc32Desc = service.getCharacteristic(SW_UPDATE_CRC32_CHARACTER)
- val updateResultDesc = service.getCharacteristic(SW_UPDATE_RESULT_CHARACTER)
+ val totalSizeDesc = getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)
+ val dataDesc = getCharacteristic(SW_UPDATE_DATA_CHARACTER)
+ val crc32Desc = getCharacteristic(SW_UPDATE_CRC32_CHARACTER)
+ val updateResultDesc = getCharacteristic(SW_UPDATE_RESULT_CHARACTER)
+
+ /// Try to set the destination region for programming (spiffs vs appload etc)
+ /// Old apploads don't have this feature, but we only fail if the user was trying to set a
+ /// spiffs - otherwise we assume appload.
+ try {
+ val updateRegionDesc = getCharacteristic(SW_UPDATE_REGION_CHARACTER)
+ sync.writeCharacteristic(
+ updateRegionDesc,
+ toNetworkByteArray(flashRegion, BluetoothGattCharacteristic.FORMAT_UINT8)
+ )
+ }
+ catch(ex: BLECharacteristicNotFoundException) {
+ errormsg("Can't set flash programming region (old appload?")
+ if(flashRegion != FLASH_REGION_APPLOAD) {
+ errormsg("Can't set flash programming region (old appload?)")
+ throw ex
+ }
+ warn("Ignoring setting appload flashRegion (old appload?")
+ }
context.assets.open(assetName).use { firmwareStream ->
val firmwareCrc = CRC32()
@@ -281,7 +332,11 @@ class SoftwareUpdateService : JobIntentService(), Logging {
// Send all the blocks
while (firmwareNumSent < firmwareSize) {
- progress = firmwareNumSent * 100 / 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
+ progress = firmwareNumSent * maxProgress / firmwareSize
debug("sending block ${progress}%")
var blockSize = 512 - 3 // Max size MTU excluding framing
@@ -320,8 +375,6 @@ class SoftwareUpdateService : JobIntentService(), Logging {
// We might get SyncContinuation timeout on the final write, assume the device simply rebooted to run the new load and we missed it
errormsg("Assuming successful update", ex)
}
-
- progress = -1 // success
}
} catch (ex: BLEException) {
progress = -3
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 fb3b113b9..ecc3cfe76 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -18,6 +18,7 @@ import com.geeksville.android.Logging
import com.geeksville.android.hideKeyboard
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
+import com.geeksville.mesh.databinding.ChannelFragmentBinding
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.ChannelOption
import com.geeksville.mesh.model.UIViewModel
@@ -25,7 +26,6 @@ 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 kotlinx.android.synthetic.main.channel_fragment.*
import java.security.SecureRandom
@@ -46,26 +46,31 @@ 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!!
+
private val model: UIViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- return inflater.inflate(R.layout.channel_fragment, container, false)
+ _binding = ChannelFragmentBinding.inflate(inflater, container, false)
+ return binding.root
}
/// Called when the lock/unlock icon has changed
private fun onEditingChanged() {
- val isEditing = editableCheckbox.isChecked
+ val isEditing = binding.editableCheckbox.isChecked
- channelOptions.isEnabled = isEditing
- shareButton.isEnabled = !isEditing
- channelNameView.isEnabled = isEditing
+ binding.channelOptions.isEnabled = isEditing
+ binding.shareButton.isEnabled = !isEditing
+ binding.channelNameView.isEnabled = isEditing
if (isEditing) // Dim the (stale) QR code while editing...
- qrView.setDim()
+ binding.qrView.setDim()
else
- qrView.setOpaque()
+ binding.qrView.setOpaque()
}
/// Pull the latest data from the model (discarding any user edits)
@@ -73,31 +78,31 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
val radioConfig = model.radioConfig.value
val channel = UIViewModel.getChannel(radioConfig)
- editableCheckbox.isChecked = false // start locked
+ binding.editableCheckbox.isChecked = false // start locked
if (channel != null) {
- qrView.visibility = View.VISIBLE
- channelNameEdit.visibility = View.VISIBLE
- channelNameEdit.setText(channel.humanName)
+ binding.qrView.visibility = View.VISIBLE
+ binding.channelNameEdit.visibility = View.VISIBLE
+ binding.channelNameEdit.setText(channel.humanName)
// 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
- editableCheckbox.isEnabled = connected
+ binding.editableCheckbox.isEnabled = connected
- qrView.setImageBitmap(channel.getChannelQR())
+ binding.qrView.setImageBitmap(channel.getChannelQR())
val modemConfig = radioConfig?.channelSettings?.modemConfig
val channelOption = ChannelOption.fromConfig(modemConfig)
- filled_exposed_dropdown.setText(
+ binding.filledExposedDropdown.setText(
getString(
channelOption?.configRes ?: R.string.modem_config_unrecognized
), false
)
} else {
- qrView.visibility = View.INVISIBLE
- channelNameEdit.visibility = View.INVISIBLE
- editableCheckbox.isEnabled = false
+ binding.qrView.visibility = View.INVISIBLE
+ binding.channelNameEdit.visibility = View.INVISIBLE
+ binding.editableCheckbox.isEnabled = false
}
onEditingChanged() // we just locked the gui
@@ -109,7 +114,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
modemConfigList
)
- filled_exposed_dropdown.setAdapter(adapter)
+ binding.filledExposedDropdown.setAdapter(adapter)
}
private fun shareChannel() {
@@ -138,17 +143,17 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- channelNameEdit.on(EditorInfo.IME_ACTION_DONE) {
+ binding.channelNameEdit.on(EditorInfo.IME_ACTION_DONE) {
requireActivity().hideKeyboard()
}
// Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing
- editableCheckbox.setOnClickListener { _ ->
- val checked = editableCheckbox.isChecked
+ binding.editableCheckbox.setOnClickListener { _ ->
+ val checked = binding.editableCheckbox.isChecked
if (checked) {
// User just unlocked for editing - remove the # goo around the channel name
UIViewModel.getChannel(model.radioConfig.value)?.let { channel ->
- channelNameEdit.setText(channel.name)
+ binding.channelNameEdit.setText(channel.name)
}
} else {
// User just locked it, we should warn and then apply changes to radio
@@ -162,7 +167,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
// Generate a new channel with only the changes the user can change in the GUI
UIViewModel.getChannel(model.radioConfig.value)?.let { old ->
val newSettings = old.settings.toBuilder()
- newSettings.name = channelNameEdit.text.toString().trim()
+ newSettings.name = binding.channelNameEdit.text.toString().trim()
// Generate a new AES256 key (for any channel not named Default)
if (!newSettings.name.equals(
@@ -183,7 +188,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
}
val selectedChannelOptionString =
- filled_exposed_dropdown.editableText.toString()
+ binding.filledExposedDropdown.editableText.toString()
val modemConfig = getModemConfig(selectedChannelOptionString)
if (modemConfig != MeshProtos.ChannelSettings.ModemConfig.UNRECOGNIZED)
@@ -199,7 +204,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
// Tell the user to try again
Snackbar.make(
- editableCheckbox,
+ binding.editableCheckbox,
R.string.radio_sleeping,
Snackbar.LENGTH_SHORT
).show()
@@ -213,7 +218,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging {
}
// Share this particular channel if someone clicks share
- shareButton.setOnClickListener {
+ binding.shareButton.setOnClickListener {
shareChannel()
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt
index 6257f6b5c..75822cdb2 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt
@@ -1,50 +1,54 @@
-package com.geeksville.mesh.ui
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.geeksville.mesh.R
-import com.geeksville.mesh.model.UIViewModel
-import kotlinx.android.synthetic.main.debug_fragment.*
-
-class DebugFragment : Fragment() {
-
- val model: UIViewModel by viewModels()
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
-
- return inflater.inflate(R.layout.debug_fragment, container, false)
- }
- //Button to clear All log
-
- //List all log
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val recyclerView = view.findViewById(R.id.packets_recyclerview)
- val adapter = PacketListAdapter(requireContext())
-
- recyclerView.adapter = adapter
- recyclerView.layoutManager = LinearLayoutManager(requireContext())
-
- clearButton.setOnClickListener {
- model.deleteAllPacket()
- }
-
- closeButton.setOnClickListener{
- parentFragmentManager.popBackStack();
- }
- model.allPackets.observe(viewLifecycleOwner, Observer {
- packets -> packets?.let { adapter.setPackets(it) }
- })
- }
+package com.geeksville.mesh.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.geeksville.mesh.R
+import com.geeksville.mesh.databinding.DebugFragmentBinding
+import com.geeksville.mesh.model.UIViewModel
+
+class DebugFragment : Fragment() {
+
+ private var _binding: DebugFragmentBinding? = null
+ // This property is only valid between onCreateView and onDestroyView.
+ private val binding get() = _binding!!
+
+ val model: UIViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ _binding = DebugFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+ //Button to clear All log
+
+ //List all log
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val recyclerView = view.findViewById(R.id.packets_recyclerview)
+ val adapter = PacketListAdapter(requireContext())
+
+ recyclerView.adapter = adapter
+ recyclerView.layoutManager = LinearLayoutManager(requireContext())
+
+ binding.clearButton.setOnClickListener {
+ model.deleteAllPacket()
+ }
+
+ binding.closeButton.setOnClickListener{
+ parentFragmentManager.popBackStack();
+ }
+ model.allPackets.observe(viewLifecycleOwner, Observer {
+ packets -> packets?.let { adapter.setPackets(it) }
+ })
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
index 2093dfcc8..dbec8b65e 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt
@@ -16,11 +16,11 @@ import com.geeksville.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.R
+import com.geeksville.mesh.databinding.AdapterMessageLayoutBinding
+import com.geeksville.mesh.databinding.MessagesFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.google.android.material.chip.Chip
-import kotlinx.android.synthetic.main.adapter_message_layout.view.*
-import kotlinx.android.synthetic.main.messages_fragment.*
import java.text.DateFormat
import java.util.*
@@ -38,13 +38,17 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
class MessagesFragment : ScreenFragment("Messages"), Logging {
+ private var _binding: MessagesFragmentBinding? = null
+ // This property is only valid between onCreateView and onDestroyView.
+ private val binding get() = _binding!!
+
private val model: UIViewModel by activityViewModels()
private val dateTimeFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
// Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access
- class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ class ViewHolder(itemView: AdapterMessageLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
val username: Chip = itemView.username
val messageText: TextView = itemView.messageText
val messageTime: TextView = itemView.messageTime
@@ -82,10 +86,10 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// Inflate the custom layout
// Inflate the custom layout
- val contactView: View = inflater.inflate(R.layout.adapter_message_layout, parent, false)
+ val contactViewBinding = AdapterMessageLayoutBinding.inflate(inflater, parent, false)
// Return a new holder instance
- return ViewHolder(contactView)
+ return ViewHolder(contactViewBinding)
}
/**
@@ -159,7 +163,7 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// scroll to the last line
if (itemCount != 0)
- messageListView.scrollToPosition(itemCount - 1)
+ binding.messageListView.scrollToPosition(itemCount - 1)
}
}
@@ -167,27 +171,28 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- return inflater.inflate(R.layout.messages_fragment, container, false)
+ _binding = MessagesFragmentBinding.inflate(inflater, container, false)
+ return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- messageInputText.on(EditorInfo.IME_ACTION_DONE) {
+ binding.messageInputText.on(EditorInfo.IME_ACTION_DONE) {
debug("did IME action")
- val str = messageInputText.text.toString().trim()
+ val str = binding.messageInputText.text.toString().trim()
if (str.isNotEmpty())
model.messagesState.sendMessage(str)
- messageInputText.setText("") // blow away the string the user just entered
+ binding.messageInputText.setText("") // blow away the string the user just entered
// requireActivity().hideKeyboard()
}
- messageListView.adapter = messagesAdapter
+ binding.messageListView.adapter = messagesAdapter
val layoutManager = LinearLayoutManager(requireContext())
layoutManager.stackFromEnd = true // We want the last rows to always be shown
- messageListView.layoutManager = layoutManager
+ binding.messageListView.layoutManager = layoutManager
model.messagesState.messages.observe(viewLifecycleOwner, Observer {
debug("New messages received: ${it.size}")
@@ -197,13 +202,13 @@ class MessagesFragment : ScreenFragment("Messages"), Logging {
// If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages
model.isConnected.observe(viewLifecycleOwner, Observer { connected ->
// If we don't know our node ID and we are offline don't let user try to send
- textInputLayout.isEnabled =
+ binding.textInputLayout.isEnabled =
connected != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null
})
model.nodeDB.myId.observe(viewLifecycleOwner, Observer { myId ->
// If we don't know our node ID and we are offline don't let user try to send
- textInputLayout.isEnabled =
+ binding.textInputLayout.isEnabled =
model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && myId != null
})
}
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 a18c4d085..889792f5b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -34,6 +34,7 @@ import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
import com.geeksville.mesh.android.bluetoothManager
import com.geeksville.mesh.android.usbManager
+import com.geeksville.mesh.databinding.SettingsFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.BluetoothInterface
import com.geeksville.mesh.service.MeshService
@@ -46,7 +47,6 @@ import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hoho.android.usbserial.driver.UsbSerialDriver
-import kotlinx.android.synthetic.main.settings_fragment.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -452,6 +452,10 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
@SuppressLint("NewApi")
class SettingsFragment : ScreenFragment("Settings"), Logging {
+ private var _binding: SettingsFragmentBinding? = null
+ // This property is only valid between onCreateView and onDestroyView.
+ private val binding get() = _binding!!
+
private val scanModel: BTScanModel by activityViewModels()
private val model: UIViewModel by activityViewModels()
@@ -478,23 +482,23 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
mainScope.handledLaunch {
debug("User started firmware update")
- updateFirmwareButton.isEnabled = false // Disable until things complete
- updateProgressBar.visibility = View.VISIBLE
- updateProgressBar.progress = 0 // start from scratch
+ binding.updateFirmwareButton.isEnabled = false // Disable until things complete
+ binding.updateProgressBar.visibility = View.VISIBLE
+ binding.updateProgressBar.progress = 0 // start from scratch
- scanStatusText.text = "Updating firmware, wait up to eight minutes..."
+ binding.scanStatusText.text = "Updating firmware, wait up to eight minutes..."
try {
service.startFirmwareUpdate()
while (service.updateStatus >= 0) {
- updateProgressBar.progress = service.updateStatus
+ binding.updateProgressBar.progress = service.updateStatus
delay(2000) // Only check occasionally
}
} finally {
val isSuccess = (service.updateStatus == -1)
- scanStatusText.text =
+ binding.scanStatusText.text =
if (isSuccess) "Update successful" else "Update failed"
- updateProgressBar.isEnabled = false
- updateFirmwareButton.isEnabled = !isSuccess
+ binding.updateProgressBar.isEnabled = false
+ binding.updateFirmwareButton.isEnabled = !isSuccess
}
}
}
@@ -504,7 +508,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- return inflater.inflate(R.layout.settings_fragment, container, false)
+ _binding = SettingsFragmentBinding.inflate(inflater, container, false)
+ return binding.root
}
private fun initNodeInfo() {
@@ -513,36 +518,36 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// If actively connected possibly let the user update firmware
val info = model.myNodeInfo.value
if (connected == MeshService.ConnectionState.CONNECTED && info != null && info.shouldUpdate && info.couldUpdate) {
- updateFirmwareButton.visibility = View.VISIBLE
- updateFirmwareButton.isEnabled = true
- updateFirmwareButton.text =
+ binding.updateFirmwareButton.visibility = View.VISIBLE
+ binding.updateFirmwareButton.isEnabled = true
+ binding.updateFirmwareButton.text =
getString(R.string.update_to).format(getString(R.string.cur_firmware_version))
} else {
- updateFirmwareButton.visibility = View.GONE
- updateProgressBar.visibility = View.GONE
+ binding.updateFirmwareButton.visibility = View.GONE
+ binding.updateProgressBar.visibility = View.GONE
}
when (connected) {
MeshService.ConnectionState.CONNECTED -> {
val fwStr = info?.firmwareString ?: ""
- scanStatusText.text = getString(R.string.connected_to).format(fwStr)
+ binding.scanStatusText.text = getString(R.string.connected_to).format(fwStr)
}
MeshService.ConnectionState.DISCONNECTED ->
- scanStatusText.text = getString(R.string.not_connected)
+ binding.scanStatusText.text = getString(R.string.not_connected)
MeshService.ConnectionState.DEVICE_SLEEP ->
- scanStatusText.text = getString(R.string.connected_sleeping)
+ binding.scanStatusText.text = getString(R.string.connected_sleeping)
}
}
/// Setup the ui widgets unrelated to BLE scanning
private fun initCommonUI() {
model.ownerName.observe(viewLifecycleOwner, Observer { name ->
- usernameEditText.setText(name)
+ binding.usernameEditText.setText(name)
})
// Only let user edit their name or set software update while connected to a radio
model.isConnected.observe(viewLifecycleOwner, Observer { connected ->
- usernameView.isEnabled = connected == MeshService.ConnectionState.CONNECTED
+ binding.usernameView.isEnabled = connected == MeshService.ConnectionState.CONNECTED
if (connected == MeshService.ConnectionState.DISCONNECTED)
model.ownerName.value = ""
initNodeInfo()
@@ -553,13 +558,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
initNodeInfo()
})
- updateFirmwareButton.setOnClickListener {
+ binding.updateFirmwareButton.setOnClickListener {
doFirmwareUpdate()
}
- usernameEditText.on(EditorInfo.IME_ACTION_DONE) {
+ binding.usernameEditText.on(EditorInfo.IME_ACTION_DONE) {
debug("did IME action")
- val n = usernameEditText.text.toString().trim()
+ val n = binding.usernameEditText.text.toString().trim()
if (n.isNotEmpty())
model.setOwner(n)
@@ -569,17 +574,17 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
val app = (requireContext().applicationContext as GeeksvilleApplication)
// Set analytics checkbox
- analyticsOkayCheckbox.isChecked = app.isAnalyticsAllowed
+ binding.analyticsOkayCheckbox.isChecked = app.isAnalyticsAllowed
- analyticsOkayCheckbox.setOnCheckedChangeListener { _, isChecked ->
+ binding.analyticsOkayCheckbox.setOnCheckedChangeListener { _, isChecked ->
debug("User changed analytics to $isChecked")
app.isAnalyticsAllowed = isChecked
- reportBugButton.isEnabled = app.isAnalyticsAllowed
+ binding.reportBugButton.isEnabled = app.isAnalyticsAllowed
}
// report bug button only enabled if analytics is allowed
- reportBugButton.isEnabled = app.isAnalyticsAllowed
- reportBugButton.setOnClickListener {
+ binding.reportBugButton.isEnabled = app.isAnalyticsAllowed
+ binding.reportBugButton.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.report_a_bug)
.setMessage(getString(R.string.report_bug_text))
@@ -600,34 +605,34 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
b.isEnabled = enabled
b.isChecked =
device.address == scanModel.selectedNotNull && device.bonded // Only show checkbox if device is still paired
- deviceRadioGroup.addView(b)
+ binding.deviceRadioGroup.addView(b)
// Once we have at least one device, don't show the "looking for" animation - it makes uers think
// something is busted
- scanProgressBar.visibility = View.INVISIBLE
+ binding.scanProgressBar.visibility = View.INVISIBLE
b.setOnClickListener {
if (!device.bonded) // If user just clicked on us, try to bond
- scanStatusText.setText(R.string.starting_pairing)
+ binding.scanStatusText.setText(R.string.starting_pairing)
b.isChecked =
scanModel.onSelected(requireActivity() as MainActivity, device)
if (!b.isSelected)
- scanStatusText.setText(getString(R.string.please_pair))
+ binding.scanStatusText.setText(getString(R.string.please_pair))
}
}
/// Show the GUI for classic scanning
private fun showClassicWidgets(visible: Int) {
- scanProgressBar.visibility = visible
- deviceRadioGroup.visibility = visible
+ binding.scanProgressBar.visibility = visible
+ binding.deviceRadioGroup.visibility = visible
}
/// Setup the GUI to do a classic (pre SDK 26 BLE scan)
private fun initClassicScan() {
// Turn off the widgets for the new API (we turn on/off hte classic widgets when we start scanning
- changeRadioButton.visibility = View.GONE
+ binding.changeRadioButton.visibility = View.GONE
showClassicWidgets(View.VISIBLE)
@@ -640,13 +645,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg ->
if (errMsg != null) {
- scanStatusText.text = errMsg
+ binding.scanStatusText.text = errMsg
}
})
scanModel.devices.observe(viewLifecycleOwner, Observer { devices ->
// Remove the old radio buttons and repopulate
- deviceRadioGroup.removeAllViews()
+ binding.deviceRadioGroup.removeAllViews()
val adapter = scanModel.bluetoothAdapter
@@ -690,14 +695,14 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null
// get rid of the warning text once at least one device is paired
- warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE
+ binding.warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE
})
}
/// Start running the modern scan, once it has one result we enable the
private fun startBackgroundScan() {
// Disable the change button until our scan has some results
- changeRadioButton.isEnabled = false
+ binding.changeRadioButton.isEnabled = false
// To skip filtering based on name and supported feature flags (UUIDs),
// don't include calls to setNamePattern() and addServiceUuid(),
@@ -726,8 +731,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
override fun onDeviceFound(chooserLauncher: IntentSender) {
debug("Found one device - enabling button")
- changeRadioButton.isEnabled = true
- changeRadioButton.setOnClickListener {
+ binding.changeRadioButton.isEnabled = true
+ binding.changeRadioButton.setOnClickListener {
debug("User clicked BLE change button")
// Request code seems to be ignored anyways
@@ -748,18 +753,18 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private fun initModernScan() {
// Turn off the widgets for the classic API
- scanProgressBar.visibility = View.GONE
- deviceRadioGroup.visibility = View.GONE
- changeRadioButton.visibility = View.VISIBLE
+ binding.scanProgressBar.visibility = View.GONE
+ binding.deviceRadioGroup.visibility = View.GONE
+ binding.changeRadioButton.visibility = View.VISIBLE
val curRadio = RadioInterfaceService.getBondedDeviceAddress(requireContext())
if (curRadio != null) {
- scanStatusText.text = getString(R.string.current_pair).format(curRadio)
- changeRadioButton.text = getString(R.string.change_radio)
+ binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio)
+ binding.changeRadioButton.text = getString(R.string.change_radio)
} else {
- scanStatusText.text = getString(R.string.not_paired_yet)
- changeRadioButton.setText(R.string.select_radio)
+ binding.scanStatusText.text = getString(R.string.not_paired_yet)
+ binding.changeRadioButton.setText(R.string.select_radio)
}
startBackgroundScan()
@@ -802,7 +807,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
debug("We have location access")
}
- locationSettingsResponse.addOnFailureListener { exception ->
+ locationSettingsResponse.addOnFailureListener { _ ->
errormsg("Failed to get location access")
// We always show the toast regardless of what type of exception we receive. Because even non
// resolvable api exceptions mean user still needs to fix something.
diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
index c3bad63f0..0b6529679 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt
@@ -14,22 +14,26 @@ import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
+import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding
+import com.geeksville.mesh.databinding.NodelistFragmentBinding
import com.geeksville.mesh.model.UIViewModel
-import kotlinx.android.synthetic.main.adapter_node_layout.view.*
-import kotlinx.android.synthetic.main.nodelist_fragment.*
import java.text.ParseException
import java.util.*
class UsersFragment : ScreenFragment("Users"), Logging {
+ private var _binding: NodelistFragmentBinding? = null
+ // This property is only valid between onCreateView and onDestroyView.
+ private val binding get() = _binding!!
+
private val model: UIViewModel by activityViewModels()
// Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access
- class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ class ViewHolder(itemView: AdapterNodeLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
val nodeNameView = itemView.nodeNameView
- val distance_view = itemView.distance_view
+ val distanceView = itemView.distanceView
val batteryPctView = itemView.batteryPercentageView
val lastTime = itemView.lastConnectionView
val powerIcon = itemView.batteryIcon
@@ -64,9 +68,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
val inflater = LayoutInflater.from(requireContext())
// Inflate the custom layout
-
- // Inflate the custom layout
- val contactView: View = inflater.inflate(R.layout.adapter_node_layout, parent, false)
+ val contactView = AdapterNodeLayoutBinding.inflate(inflater, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
@@ -108,10 +110,10 @@ class UsersFragment : ScreenFragment("Users"), Logging {
val ourNodeInfo = model.nodeDB.ourNodeInfo
val distance = ourNodeInfo?.distanceStr(n)
if (distance != null) {
- holder.distance_view.text = distance
- holder.distance_view.visibility = View.VISIBLE
+ holder.distanceView.text = distance
+ holder.distanceView.visibility = View.VISIBLE
} else {
- holder.distance_view.visibility = View.INVISIBLE
+ holder.distanceView.visibility = View.INVISIBLE
}
renderBattery(n.batteryPctLevel, holder)
@@ -176,14 +178,15 @@ class UsersFragment : ScreenFragment("Users"), Logging {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- return inflater.inflate(R.layout.nodelist_fragment, container, false)
+ _binding = NodelistFragmentBinding.inflate(inflater, container, false)
+ return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- nodeListView.adapter = nodesAdapter
- nodeListView.layoutManager = LinearLayoutManager(requireContext())
+ binding.nodeListView.adapter = nodesAdapter
+ binding.nodeListView.layoutManager = LinearLayoutManager(requireContext())
model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { it ->
nodesAdapter.onNodesChanged(it.values)
diff --git a/app/src/main/proto b/app/src/main/proto
index a0b8d8889..aac0044b2 160000
--- a/app/src/main/proto
+++ b/app/src/main/proto
@@ -1 +1 @@
-Subproject commit a0b8d888961720d70ab7467c94d8fa0687e58020
+Subproject commit aac0044b2dcca5daa86c6532c1d8c43475956d31
diff --git a/build.gradle b/build.gradle
index 443c5418e..f37671e92 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.4.10'
+ ext.kotlin_version = '1.4.20'
ext.coroutines_version = "1.3.9"
repositories {
@@ -21,10 +21,10 @@ buildscript {
// Check that you have the Google Services Gradle plugin v4.3.2 or later
// (if not, add it).
classpath 'com.google.gms:google-services:4.3.4'
- classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'
+ classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1'
// protobuf plugin - docs here https://github.com/google/protobuf-gradle-plugin
- classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.14'
//classpath "app.brant:amazonappstorepublisher:0.1.0"
classpath 'com.github.triplet.gradle:play-publisher:2.8.0'
diff --git a/geeksville-androidlib b/geeksville-androidlib
index af1a758b0..534f0e192 160000
--- a/geeksville-androidlib
+++ b/geeksville-androidlib
@@ -1 +1 @@
-Subproject commit af1a758b0d4ed0b98e412d0aa03195d30f95127a
+Subproject commit 534f0e192bbbaaa6c32a981534b00451ed708ddc