kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
commit
57aff30082
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) })
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue