diff --git a/app/build.gradle b/app/build.gradle index 47f1f9501..5cac80212 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ apply plugin: 'kotlin-parcelize' apply plugin: 'kotlinx-serialization' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.github.triplet.play' +apply plugin: 'de.mobilej.unmock' // apply plugin: "app.brant.amazonappstorepublisher" // Apply the Crashlytics Gradle plugin @@ -14,6 +15,11 @@ apply plugin: 'com.google.protobuf' apply plugin: 'kotlin-kapt' +unMock { + keep "android.net.Uri" + keep "android.util.Base64" +} + android { /* signingConfigs { @@ -31,8 +37,8 @@ android { applicationId "com.geeksville.mesh" minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works) targetSdkVersion 29 - versionCode 20207 // format is Mmmss (where M is 1+the numeric major number - versionName "1.2.7" + versionCode 20211 // format is Mmmss (where M is 1+the numeric major number + versionName "1.2.11" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // per https://developer.android.com/studio/write/vector-asset-studio diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index ecb07e1a1..1acff242c 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -668,8 +668,7 @@ class MainActivity : AppCompatActivity(), Logging, else { val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0") - val minVer = DeviceVersion("1.2.0") - if (curVer < minVer) + 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 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 5686bb209..665a8de25 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -1,15 +1,7 @@ package com.geeksville.mesh.model -import android.graphics.Bitmap -import android.net.Uri -import android.util.Base64 import com.geeksville.mesh.ChannelProtos -import com.geeksville.mesh.MeshProtos import com.google.protobuf.ByteString -import com.google.zxing.BarcodeFormat -import com.google.zxing.MultiFormatWriter -import com.journeyapps.barcodescanner.BarcodeEncoder -import java.net.MalformedURLException /** Utility function to make it easy to declare byte arrays - FIXME move someplace better */ fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } @@ -19,19 +11,15 @@ data class Channel( val settings: ChannelProtos.ChannelSettings = ChannelProtos.ChannelSettings.getDefaultInstance() ) { companion object { - // Note: this string _SHOULD NOT BE LOCALIZED_ because it directly hashes to values used on the device for the default channel name. - // FIXME - make this work with new channel name system - const val defaultChannelName = "Default" - // These bytes must match the well known and not secret bytes used the default channel AES128 key device code val channelDefaultKey = byteArrayOfInts( 0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf ) - // Placeholder when emulating - val emulated = Channel( - ChannelProtos.ChannelSettings.newBuilder().setName(defaultChannelName) + // TH=he unsecured channel that devices ship with + val defaultChannel = Channel( + ChannelProtos.ChannelSettings.newBuilder() .setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build() ) } @@ -78,14 +66,13 @@ data class Channel( */ val humanName: String get() { - val suffix: Char = if (settings.psk.size() != 1) { - // we have a full PSK, so hash it to generate the suffix - val code = settings.psk.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) }) - - 'A' + (code % 26) - } else - '0' + settings.psk.byteAt(0).toInt() + // start with the PSK then xor in the name + val pskCode = xorHash(psk.toByteArray()) + val nameCode = xorHash(name.toByteArray()) + val suffix = 'A' + ((pskCode xor nameCode) % 26) return "#${name}-${suffix}" } } + +fun xorHash(b: ByteArray) = b.fold(0, { acc, x -> acc xor (x.toInt() and 0xff) }) \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt index 59269184a..3fa0b4697 100644 --- a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt +++ b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt @@ -16,11 +16,6 @@ data class ChannelSet( ) { companion object { - // Placeholder when emulating - val emulated = ChannelSet( - AppOnlyProtos.ChannelSet.newBuilder().addSettings(Channel.emulated.settings).build() - ) - const val prefix = "https://www.meshtastic.org/d/#" private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING 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 2567c93ba..b58f90d56 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -94,6 +94,10 @@ class MeshService : Service(), Logging { "com.geeksville.mesh", "com.geeksville.mesh.service.MeshService" ) + + /** The minimmum firmware version we know how to talk to. We'll still be able to talk to 1.0 firmwares but only well enough to ask them to firmware update + */ + val minFirmwareVersion = DeviceVersion("1.2.0") } enum class ConnectionState { @@ -167,7 +171,7 @@ class MeshService : Service(), Logging { // FIXME - currently we don't support location reading without google play if (fusedLocationClient == null && isGooglePlayAvailable(this)) { GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS - + val request = LocationRequest.create().apply { interval = requestInterval priority = LocationRequest.PRIORITY_HIGH_ACCURACY @@ -945,13 +949,13 @@ class MeshService : Service(), Logging { } private var locationRequestInterval: Long = 0; - private fun setupLocationRequest () { + private fun setupLocationRequest() { val desiredInterval: Long = if (myNodeInfo?.hasGPS == true) { 0L // no requests when device has GPS - } else if (numOnlineNodes < 2) { + } else if (numOnlineNodes < 2) { 5 * 60 * 1000L // send infrequently, device needs these requests to set its clock } else { - radioConfig?.preferences?.positionBroadcastSecs?.times( 1000L) ?: 5 * 60 * 1000L + radioConfig?.preferences?.positionBroadcastSecs?.times(1000L) ?: 5 * 60 * 1000L } debug("desired location request $desiredInterval, current $locationRequestInterval") @@ -1174,18 +1178,6 @@ class MeshService : Service(), Logging { /// Used to make sure we never get foold by old BLE packets private var configNonce = 1 - - private fun handleRadioConfig(radio: RadioConfigProtos.RadioConfig) { - val packetToSave = Packet( - UUID.randomUUID().toString(), - "RadioConfig", - System.currentTimeMillis(), - radio.toString() - ) - insertPacket(packetToSave) - radioConfig = radio - } - /** * Convert a protobuf NodeInfo into our model objects and update our node DB */ @@ -1296,6 +1288,9 @@ class MeshService : Service(), Logging { } } + /// If found, the old region string of the form 1.0-EU865 etc... + private var legacyRegion: String? = null + /** * Update the nodeinfo (called from either new API version or the old one) */ @@ -1309,6 +1304,7 @@ class MeshService : Service(), Logging { insertPacket(packetToSave) rawMyNodeInfo = myInfo + legacyRegion = myInfo.region regenMyNodeInfo() // We'll need to get a new set of channels and settings now @@ -1342,34 +1338,37 @@ class MeshService : Service(), Logging { } if (curRegionValue == RadioConfigProtos.RegionCode.Unset_VALUE) { - TODO("Need gui for setting region") - /* // look for a legacy region + // look for a legacy region val legacyRegex = Regex(".+-(.+)") - myNodeInfo?.region?.let { legacyRegion -> - val matches = legacyRegex.find(legacyRegion) + legacyRegion?.let { lr -> + val matches = legacyRegex.find(lr) if (matches != null) { val (region) = matches.destructured val newRegion = RadioConfigProtos.RegionCode.valueOf(region) info("Upgrading legacy region $newRegion (code ${newRegion.number})") curRegionValue = newRegion.number } - } */ + } } // 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) { - info("Telling device to upgrade region") + 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() + // 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() + val newPrefs = currentConfig.preferences.toBuilder() + newPrefs.regionValue = curRegionValue + newConfig.preferences = newPrefs.build() - sendRadioConfig(newConfig.build()) + sendRadioConfig(newConfig.build()) + } } + else + warn("Device is too old to understand region changes") } } } @@ -1413,10 +1412,14 @@ class MeshService : Service(), Logging { regenMyNodeInfo() // we have a node db now, so can possibly find a better hwmodel myNodeInfo = newMyNodeInfo // we might have just updated myNodeInfo - + sendAnalytics() - requestRadioConfig() + if (deviceVersion < minFirmwareVersion) { + info("Device firmware is too old, faking config so firmware update can occur") + onHasSettings() + } else + requestRadioConfig() } } else warn("Ignoring stale config complete") 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 bc32450c5..9ca623088 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -169,12 +169,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { .setPositiveButton(getString(R.string.accept)) { _, _ -> // Generate a new channel with only the changes the user can change in the GUI model.channels.value?.primaryChannel?.let { oldPrimary -> - val newSettings = oldPrimary.settings.toBuilder() + var newSettings = oldPrimary.settings.toBuilder() newSettings.name = binding.channelNameEdit.text.toString().trim() - // Generate a new AES256 key (for any channel not named Default) + // Generate a new AES256 key unleess the user is trying to go back to stock if (!newSettings.name.equals( - Channel.defaultChannelName, + Channel.defaultChannel.name, ignoreCase = true ) ) { @@ -184,10 +184,8 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { random.nextBytes(bytes) newSettings.psk = ByteString.copyFrom(bytes) } else { - debug("ASSIGNING NEW default AES128 KEY") - newSettings.name = - Channel.defaultChannelName // Fix any case errors - newSettings.psk = ByteString.copyFrom(Channel.channelDefaultKey) + debug("Switching back to default channel") + newSettings = Channel.defaultChannel.settings.toBuilder() } val selectedChannelOptionString = diff --git a/app/src/test/java/com/geeksville/mesh/NodeInfoTest.kt b/app/src/test/java/com/geeksville/mesh/NodeInfoTest.kt index aa9059e57..cf6713cc9 100644 --- a/app/src/test/java/com/geeksville/mesh/NodeInfoTest.kt +++ b/app/src/test/java/com/geeksville/mesh/NodeInfoTest.kt @@ -8,9 +8,10 @@ import org.junit.Test import java.util.* class NodeInfoTest { - val ni1 = NodeInfo(4, MeshUser("+one", "User One", "U1"), Position(37.1, 121.1, 35)) - val ni2 = NodeInfo(5, MeshUser("+two", "User Two", "U2"), Position(37.11, 121.1, 40)) - val ni3 = NodeInfo(6, MeshUser("+three", "User Three", "U3"), Position(37.101, 121.1, 40)) + val model = MeshProtos.HardwareModel.ANDROID_SIM + val ni1 = NodeInfo(4, MeshUser("+one", "User One", "U1", model), Position(37.1, 121.1, 35)) + val ni2 = NodeInfo(5, MeshUser("+two", "User Two", "U2", model), Position(37.11, 121.1, 40)) + val ni3 = NodeInfo(6, MeshUser("+three", "User Three", "U3", model), Position(37.101, 121.1, 40)) private val currentDefaultLocale = LocaleListCompat.getDefault().get(0) diff --git a/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt b/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt new file mode 100644 index 000000000..bf41360d8 --- /dev/null +++ b/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt @@ -0,0 +1,17 @@ +package com.geeksville.mesh.model + +import android.net.Uri +import org.junit.Assert +import org.junit.Test + +class ChannelSetTest { + /** make sure we match the python and device code behavior */ + @Test + fun matchPython() { + val url = Uri.parse("https://www.meshtastic.org/d/#CgUYAyIBAQ") + val cs = ChannelSet(url) + Assert.assertEquals("LongSlow", cs.primaryChannel!!.name, ) + Assert.assertEquals("#LongSlow-V", cs.primaryChannel!!.humanName, ) + Assert.assertEquals(url, cs.getChannelUrl(false)) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshServiceTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshServiceTest.kt index f8db6328c..aabea7598 100644 --- a/app/src/test/java/com/geeksville/mesh/service/MeshServiceTest.kt +++ b/app/src/test/java/com/geeksville/mesh/service/MeshServiceTest.kt @@ -1,31 +1,32 @@ -package com.geeksville.mesh.service - -import com.geeksville.mesh.MeshUser -import com.geeksville.mesh.NodeInfo -import com.geeksville.mesh.Position -import org.junit.Assert -import org.junit.Test - - -class MeshServiceTest { - - val nodeInfo = NodeInfo(4, MeshUser("+one", "User One", "U1"), Position(37.1, 121.1, 35, 10)) - - @Test - fun givenNodeInfo_whenUpdatingWithNewTime_thenPositionTimeIsUpdated() { - - val newerTime = 20 - updateNodeInfoTime(nodeInfo, newerTime) - Assert.assertEquals(newerTime, nodeInfo.position?.time) - } - - @Test - fun givenNodeInfo_whenUpdatingWithOldTime_thenPositionTimeIsNotUpdated() { - val olderTime = 5 - val timeBeforeTryingToUpdate = nodeInfo.position?.time - updateNodeInfoTime(nodeInfo, olderTime) - Assert.assertEquals(timeBeforeTryingToUpdate, nodeInfo.position?.time) - } -} - - +package com.geeksville.mesh.service + +import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.MeshUser +import com.geeksville.mesh.NodeInfo +import com.geeksville.mesh.Position +import org.junit.Assert +import org.junit.Test + + +class MeshServiceTest { + val model = MeshProtos.HardwareModel.ANDROID_SIM + val nodeInfo = NodeInfo(4, MeshUser("+one", "User One", "U1", model), Position(37.1, 121.1, 35, 10)) + + @Test + fun givenNodeInfo_whenUpdatingWithNewTime_thenPositionTimeIsUpdated() { + + val newerTime = 20 + updateNodeInfoTime(nodeInfo, newerTime) + Assert.assertEquals(newerTime, nodeInfo.position?.time) + } + + @Test + fun givenNodeInfo_whenUpdatingWithOldTime_thenPositionTimeIsNotUpdated() { + val olderTime = 5 + val timeBeforeTryingToUpdate = nodeInfo.position?.time + updateNodeInfoTime(nodeInfo, olderTime) + Assert.assertEquals(timeBeforeTryingToUpdate, nodeInfo.position?.time) + } +} + + diff --git a/build.gradle b/build.gradle index 183d7bf8d..ba99badad 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" @@ -28,6 +28,9 @@ buildscript { //classpath "app.brant:amazonappstorepublisher:0.1.0" classpath 'com.github.triplet.gradle:play-publisher:2.8.0' + + // for unit testing https://github.com/bjoernQ/unmock-plugin + classpath 'com.github.bjoernq:unmockplugin:0.7.6' } }