diff options
| author | Albin <albin@mullvad.net> | 2024-02-15 14:31:19 +0100 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2024-02-15 14:31:19 +0100 |
| commit | d42287a49451c6ab42efb0edc5e66a1375e28306 (patch) | |
| tree | c13dbdc286dfc9740a7d19b8c2c03e578817cd46 /android/lib/model | |
| parent | d0b49816b95dbf7b6a5e8048caef61212191d07d (diff) | |
| parent | db4179285e29c0c94b9195f7579ef17daaa8b719 (diff) | |
| download | mullvadvpn-d42287a49451c6ab42efb0edc5e66a1375e28306.tar.xz mullvadvpn-d42287a49451c6ab42efb0edc5e66a1375e28306.zip | |
Merge branch 'android-gl-maps'
Diffstat (limited to 'android/lib/model')
9 files changed, 487 insertions, 5 deletions
diff --git a/android/lib/model/build.gradle.kts b/android/lib/model/build.gradle.kts index 040c2fca0c..7264c6041a 100644 --- a/android/lib/model/build.gradle.kts +++ b/android/lib/model/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id(Dependencies.Plugin.androidLibraryId) + id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5 id(Dependencies.Plugin.kotlinAndroidId) id(Dependencies.Plugin.kotlinParcelizeId) } @@ -8,7 +9,10 @@ android { namespace = "net.mullvad.mullvadvpn.model" compileSdk = Versions.Android.compileSdkVersion - defaultConfig { minSdk = Versions.Android.minSdkVersion } + defaultConfig { + minSdk = Versions.Android.minSdkVersion + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -30,4 +34,12 @@ dependencies { implementation(Dependencies.jodaTime) implementation(Dependencies.Kotlin.stdlib) implementation(Dependencies.KotlinX.coroutinesAndroid) + + // Test dependencies + testRuntimeOnly(Dependencies.junitEngine) + + testImplementation(Dependencies.Kotlin.test) + testImplementation(Dependencies.junitApi) + + testImplementation(project(Dependencies.Mullvad.commonTestLib)) } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt index e15ab20376..625de76b29 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt @@ -10,5 +10,7 @@ data class GeoIpLocation( val ipv6: InetAddress?, val country: String, val city: String?, - val hostname: String? + val latitude: Double, + val longitude: Double, + val hostname: String?, ) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt index 04f92a72ac..386257a72a 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt @@ -9,20 +9,20 @@ sealed class GeographicLocationConstraint : Parcelable { @Parcelize data class Country(val countryCode: String) : GeographicLocationConstraint() { override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, null, null) + get() = GeoIpLocation(null, null, countryCode, null, 0.0, 0.0, null) } @Parcelize data class City(val countryCode: String, val cityCode: String) : GeographicLocationConstraint() { override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, cityCode, null) + get() = GeoIpLocation(null, null, countryCode, cityCode, 0.0, 0.0, null) } @Parcelize data class Hostname(val countryCode: String, val cityCode: String, val hostname: String) : GeographicLocationConstraint() { override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, cityCode, hostname) + get() = GeoIpLocation(null, null, countryCode, cityCode, 0.0, 0.0, hostname) } } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LatLong.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LatLong.kt new file mode 100644 index 0000000000..ae047130e8 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LatLong.kt @@ -0,0 +1,21 @@ +package net.mullvad.mullvadvpn.model + +import kotlin.math.pow +import kotlin.math.sqrt + +data class LatLong(val latitude: Latitude, val longitude: Longitude) { + + fun distanceTo(other: LatLong): Float = + sqrt( + latitude.distanceTo(other.latitude).pow(2f) + + (longitude.distanceTo(other.longitude).pow(2f)) + ) + + operator fun plus(other: LatLong) = + LatLong(latitude + other.latitude, longitude + other.longitude) + + operator fun minus(other: LatLong) = + LatLong(latitude - other.latitude, longitude - other.longitude) +} + +const val COMPLETE_ANGLE = 360f diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Latitude.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Latitude.kt new file mode 100644 index 0000000000..14c5b66983 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Latitude.kt @@ -0,0 +1,55 @@ +package net.mullvad.mullvadvpn.model + +import kotlin.math.absoluteValue + +@JvmInline +value class Latitude(val value: Float) { + init { + require(value in LATITUDE_RANGE) { + "Latitude: '$value' must be between $MIN_LATITUDE_VALUE and $MAX_LATITUDE_VALUE" + } + } + + fun distanceTo(other: Latitude) = (other.value - value).absoluteValue + + operator fun plus(other: Latitude) = fromFloat(value + other.value) + + operator fun minus(other: Latitude) = fromFloat(value - other.value) + + companion object { + private const val MIN_LATITUDE_VALUE: Float = -90f + private const val MAX_LATITUDE_VALUE: Float = 90f + private val LATITUDE_RANGE = MIN_LATITUDE_VALUE..MAX_LATITUDE_VALUE + + /** + * Create a [Latitude] from a float value. + * + * This function will unwind a float to a valid latitude value. E.g 190 will be unwound to + * -10 and 360 will be unwound to 0. + */ + fun fromFloat(value: Float): Latitude { + val unwoundValue = unwind(value) + return Latitude(unwoundValue) + } + + private fun unwind(value: Float): Float { + // Remove all 360 degrees + val withoutRotations = value % COMPLETE_ANGLE + + // If we are above 180 or below -180, we wrapped half a turn and need to flip sign + val partiallyUnwound = + if (withoutRotations.absoluteValue > COMPLETE_ANGLE / 2) { + -withoutRotations % (COMPLETE_ANGLE / 2) + } else withoutRotations + + return when { + partiallyUnwound < MIN_LATITUDE_VALUE -> + MIN_LATITUDE_VALUE - (partiallyUnwound % MIN_LATITUDE_VALUE) + partiallyUnwound > MAX_LATITUDE_VALUE -> + MAX_LATITUDE_VALUE - (partiallyUnwound % MAX_LATITUDE_VALUE) + // partiallyUnwound in range MIN_LATITUDE_VALUE..MAX_LATITUDE_VALUE + else -> partiallyUnwound + } + } + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Longitude.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Longitude.kt new file mode 100644 index 0000000000..9f73a6ff17 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Longitude.kt @@ -0,0 +1,55 @@ +package net.mullvad.mullvadvpn.model + +import kotlin.math.absoluteValue + +@JvmInline +value class Longitude(val value: Float) { + init { + require(value in LONGITUDE_RANGE) { + "Longitude: '$value' must be between $MIN_LONGITUDE_VALUE and $MAX_LONGITUDE_VALUE" + } + } + + fun distanceTo(other: Longitude) = vectorTo(other).value.absoluteValue + + fun vectorTo(other: Longitude): Longitude { + val diff = other.value - value + val vectorValue = + when { + diff > MAX_LONGITUDE_VALUE -> diff - COMPLETE_ANGLE + diff < MIN_LONGITUDE_VALUE -> diff + COMPLETE_ANGLE + else -> diff + } + return Longitude(vectorValue) + } + + operator fun plus(other: Longitude) = fromFloat(value + other.value) + + operator fun minus(other: Longitude) = fromFloat(value - other.value) + + companion object { + private const val MIN_LONGITUDE_VALUE: Float = -180f + private const val MAX_LONGITUDE_VALUE: Float = 180f + private val LONGITUDE_RANGE = MIN_LONGITUDE_VALUE..MAX_LONGITUDE_VALUE + + /** + * Create a [Longitude] from a float value. + * + * This function will unwind a float to a valid longitude value. E.g 190 will be unwound to + * -170 and 360 will be unwound to 0. + */ + fun fromFloat(value: Float): Longitude { + val unwoundValue = unwind(value) + return Longitude(unwoundValue) + } + + private fun unwind(value: Float): Float { + val unwound = value % COMPLETE_ANGLE + return when { + unwound > MAX_LONGITUDE_VALUE -> unwound - COMPLETE_ANGLE + unwound < MIN_LONGITUDE_VALUE -> unwound + COMPLETE_ANGLE + else -> unwound + } + } + } +} diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatLongTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatLongTest.kt new file mode 100644 index 0000000000..6644e25e82 --- /dev/null +++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatLongTest.kt @@ -0,0 +1,20 @@ +package net.mullvad.mullvadvpn.model + +import kotlin.math.sqrt +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class LatLongTest { + + @Test + fun `distance between two LatLong should be same as hypotenuse`() { + val latLong1 = LatLong(Latitude(30f), Longitude(40f)) + val latLong2 = LatLong(Latitude(-40f), Longitude(170f)) + + val latDiff = latLong1.latitude.distanceTo(latLong2.latitude) + val longDiff = latLong1.longitude.distanceTo(latLong2.longitude) + val hypotenuse = sqrt(latDiff * latDiff + longDiff * longDiff) + + assertEquals(hypotenuse, latLong1.distanceTo(latLong2)) + } +} diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt new file mode 100644 index 0000000000..8788c2123a --- /dev/null +++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt @@ -0,0 +1,163 @@ +package net.mullvad.mullvadvpn.model + +import kotlin.math.absoluteValue +import kotlin.test.assertEquals +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class LatitudeTest { + @Test + fun `creating a valid latitude should work`() { + assertDoesNotThrow { Latitude(30f) } + } + + @Test + fun `creating a valid negative latitude should work`() { + assertDoesNotThrow { Latitude(-30f) } + } + + @Test + fun `create with too high latitude should give IllegalArgumentException`() { + assertThrows<IllegalArgumentException> { Latitude(90.1f) } + } + + @Test + fun `create with too low latitude should give IllegalArgumentException`() { + assertThrows<IllegalArgumentException> { Latitude(-90.1f) } + } + + @Test + fun `fromFloat should accept and wrap large value`() { + val longFloat = 400f + val longitude = Latitude.fromFloat(longFloat) + + assertEquals(40f, longitude.value) + } + + @Test + fun `fromFloat should accept and support half-wrap`() { + val longFloat = 100f + val longitude = Latitude.fromFloat(longFloat) + + assertEquals(80f, longitude.value) + } + + @Test + fun `fromFloat should accept and support negative half-wrap`() { + val longFloat = -100f + val longitude = Latitude.fromFloat(longFloat) + + assertEquals(-80f, longitude.value) + } + + @Test + fun `adding two positive latitude should result in the sum`() { + val latFloat1 = 20f + val latitude1 = Latitude(latFloat1) + val latFloat2 = 30f + val latitude2 = Latitude(latFloat2) + + assertEquals(latFloat1 + latFloat2, (latitude1 + latitude2).value) + } + + @Test + fun `adding two large positive latitude should result in the sum wrapped`() { + val latFloat1 = 70f + val latitude1 = Latitude(latFloat1) + val latFloat2 = 50f + val latitude2 = Latitude(latFloat2) + + val expectedResult = 60f + + assertEquals(expectedResult, (latitude1 + latitude2).value) + } + + @Test + fun `adding two negative latitude should result in the sum`() { + val latFloat1 = -20f + val latitude1 = Latitude(latFloat1) + val latFloat2 = -40f + val latitude2 = Latitude(latFloat2) + + assertEquals(latFloat1 + latFloat2, (latitude1 + latitude2).value) + } + + @Test + fun `adding two large negative latitude should result in the sum`() { + val latFloat1 = -70f + val latitude1 = Latitude(latFloat1) + val latFloat2 = -50f + val latitude2 = Latitude(latFloat2) + + val expectedResult = -60f + + assertEquals(expectedResult, (latitude1 + latitude2).value) + } + + @Test + fun `subtracting two positive latitude should result in the sum`() { + val latFloat1 = 80f + val latitude1 = Latitude(latFloat1) + val latFloat2 = 30f + val latitude2 = Latitude(latFloat2) + + assertEquals(latFloat1 - latFloat2, (latitude1 - latitude2).value) + } + + @Test + fun `subtracting a large latitude should result in the sum wrapped`() { + val latFloat1 = -30f + val latitude1 = Latitude(latFloat1) + val latFloat2 = 80f + val latitude2 = Latitude(latFloat2) + + val expectedResult = -70f + + assertEquals(expectedResult, (latitude1 - latitude2).value) + } + + @Test + fun `subtracting a negative latitude should result in same as addition`() { + val latFloat1 = -30f + val latitude1 = Latitude(latFloat1) + val latFloat2 = -40f + val latitude2 = Latitude(latFloat2) + + assertEquals(latFloat1 + latFloat2.absoluteValue, (latitude1 - latitude2).value) + } + + @Test + fun `subtracting a large negative latitude should result in same as addition wrapped`() { + val latFloat1 = 80f + val latitude1 = Latitude(latFloat1) + val latFloat2 = -90f + val latitude2 = Latitude(latFloat2) + + val absoluteLatitude2 = Latitude.fromFloat(latFloat2.absoluteValue) + + assertEquals(latitude1 + absoluteLatitude2, latitude1 - latitude2) + } + + @Test + fun `distanceTo with two positive latitudes`() { + val latFloat1 = 80f + val latitude1 = Latitude(latFloat1) + val latFloat2 = 30f + val latitude2 = Latitude(latFloat2) + + assertEquals(latFloat1 - latFloat2, latitude1.distanceTo(latitude2)) + } + + @Test + fun `distanceTo with two negative latitudes`() { + val latFloat1 = -80f + val latitude1 = Latitude(latFloat1) + val latFloat2 = -30f + val latitude2 = Latitude(latFloat2) + + val expectedValue = 50f + + assertEquals(expectedValue, latitude1.distanceTo(latitude2)) + } +} diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt new file mode 100644 index 0000000000..de94661ad0 --- /dev/null +++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt @@ -0,0 +1,154 @@ +package net.mullvad.mullvadvpn.model + +import kotlin.math.absoluteValue +import kotlin.test.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class LongitudeTest { + @Test + fun `create longitude with longitude should work`() { + assertDoesNotThrow { Longitude(80f) } + } + + @Test + fun `create longitude with negative longitude should work`() { + assertDoesNotThrow { Longitude(-80f) } + } + + @Test + fun `create too high longitude should give IllegalArgumentException`() { + assertThrows<IllegalArgumentException> { Longitude(180.1f) } + } + + @Test + fun `create too low longitude should give IllegalArgumentException`() { + assertThrows<IllegalArgumentException> { Longitude(-180.1f) } + } + + @Test + fun `fromFloat should accept and wrap large value`() { + val longFloat = 720f + val longitude = Longitude.fromFloat(longFloat) + + assertEquals(0f, longitude.value) + } + + @Test + fun `fromFloat should accept and wrap large negative value`() { + val longFloat = -720f + val longitude = Longitude.fromFloat(longFloat) + + assertEquals(0f, longitude.value, 0f) + } + + @Test + fun `adding two positive longitude should result in the sum`() { + val longFloat1 = 80f + val longitude1 = Longitude(longFloat1) + val longFloat2 = 30f + val longitude2 = Longitude(longFloat2) + + assertEquals(longFloat1 + longFloat2, (longitude1 + longitude2).value) + } + + @Test + fun `adding two large positive longitude should result in the sum wrapped`() { + val longFloat1 = 170f + val longitude1 = Longitude(longFloat1) + val longFloat2 = 150f + val longitude2 = Longitude(longFloat2) + + val expectedResult = -40f + + assertEquals(expectedResult, (longitude1 + longitude2).value) + } + + @Test + fun `adding two negative longitude should result in the sum wrapped`() { + val longFloat1 = -80f + val longitude1 = Longitude(longFloat1) + val longFloat2 = -40f + val longitude2 = Longitude(longFloat2) + + assertEquals(longFloat1 + longFloat2, (longitude1 + longitude2).value) + } + + @Test + fun `subtracting two positive longitude should result in the sum`() { + val longFloat1 = 80f + val longitude1 = Longitude(longFloat1) + val longFloat2 = 30f + val longitude2 = Longitude(longFloat2) + + assertEquals(longFloat1 - longFloat2, (longitude1 - longitude2).value) + } + + @Test + fun `subtracting a large longitude should result in the sum wrapped`() { + val longFloat1 = -30f + val longitude1 = Longitude(longFloat1) + val longFloat2 = 170f + val longitude2 = Longitude(longFloat2) + + val expectedResult = 160f + + assertEquals(expectedResult, (longitude1 - longitude2).value) + } + + @Test + fun `subtracting a negative latitude should result in same as addition`() { + val longFloat1 = -80f + val longitude1 = Longitude(longFloat1) + val longFloat2 = -40f + val longitude2 = Longitude(longFloat2) + + assertEquals(longFloat1 + longFloat2.absoluteValue, (longitude1 - longitude2).value) + } + + @Test + fun `subtracting a large negative latitude should result in same as addition wrapped`() { + val longFloat1 = 80f + val longitude1 = Longitude(longFloat1) + val longFloat2 = -140f + val longitude2 = Longitude(longFloat2) + + val absoluteLongitude2 = Longitude.fromFloat(longFloat2.absoluteValue) + assertEquals(longitude1 + absoluteLongitude2, longitude1 - longitude2) + } + + @Test + fun `distanceTo with two positive longitudes`() { + val longFloat1 = 80f + val longitude1 = Longitude(longFloat1) + val longFloat2 = 30f + val longitude2 = Longitude(longFloat2) + + assertEquals(longFloat1 - longFloat2, longitude1.distanceTo(longitude2)) + } + + @Test + fun `distanceTo with two negative longitudes`() { + val longFloat1 = -80f + val longitude1 = Longitude(longFloat1) + val longFloat2 = -30f + val longitude2 = Longitude(longFloat2) + + val expectedValue = 50f + + assertEquals(expectedValue, longitude1.distanceTo(longitude2)) + } + + @Test + fun `distanceTo with wrapping value as shortest path`() { + val longFloat1 = -170f + val longitude1 = Longitude(longFloat1) + val longFloat2 = 170f + val longitude2 = Longitude(longFloat2) + + val expectedValue = 20f + + assertEquals(expectedValue, longitude1.distanceTo(longitude2)) + } +} |
