diff --git a/IAN/udp/konsist/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkTypeKonsistTest.kt b/IAN/udp/konsist/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkTypeKonsistTest.kt index d71a295..e7ae744 100644 --- a/IAN/udp/konsist/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkTypeKonsistTest.kt +++ b/IAN/udp/konsist/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkTypeKonsistTest.kt @@ -3,7 +3,6 @@ package com.walkertribe.ian.protocol.udp import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.declaration.KoEnumConstantDeclaration import com.lemonappdev.konsist.api.ext.list.enumConstants -import com.lemonappdev.konsist.api.ext.list.withName import com.lemonappdev.konsist.api.ext.list.withRepresentedTypeOf import com.lemonappdev.konsist.api.verify.assertTrue import io.kotest.core.spec.style.DescribeSpec @@ -30,12 +29,23 @@ private enum class NamingConventionTest(val testName: String) { type.assertTrue { it.hasNameEndingWith("BIT_BLOCK") } } }, + BROADCAST_ADDRESS("Broadcast address property follows naming patterns") { + override fun test(type: KoEnumConstantDeclaration) { + val prefix = type.name.substringBeforeLast("BLOCK") + type.assertTrue { + it.hasVariable { prop -> + prop.name == "broadcastAddress" && prop.text.contains("${prefix}BROADCAST") + } + } + } + }, CONSTRAINTS("Constraints property follows naming patterns") { override fun test(type: KoEnumConstantDeclaration) { val prefix = type.name.substringBeforeLast("BLOCK") - val constraints = type.variables.withName("constraints") - constraints.first().assertTrue { prop -> - prop.text.contains("${prefix}CONSTRAINTS") + type.assertTrue { + it.hasVariable { prop -> + prop.name == "constraints" && prop.text.contains("${prefix}CONSTRAINTS") + } } } }; diff --git a/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkAddress.kt b/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkAddress.kt deleted file mode 100644 index ada190f..0000000 --- a/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkAddress.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.walkertribe.ian.protocol.udp - -import kotlinx.io.IOException -import java.net.NetworkInterface - -/** - * A class which contains all the information needed to perform and respond to UDP server discovery - * broadcasts. - * @author rjwut - */ -class PrivateNetworkAddress private constructor( - /** - * Returns the broadcast address. - */ - val hostAddress: String -) { - companion object { - private const val DEFAULT_HOST = "255.255.255.255" - - val DEFAULT by lazy { PrivateNetworkAddress(DEFAULT_HOST) } - - /** - * Returns a [PrivateNetworkAddress] believed to represent the best one to represent this - * machine on the LAN, or null if none can be found. - */ - @Throws(IOException::class) - fun guessBest(): PrivateNetworkAddress? = findAll().firstOrNull() - - /** - * Returns a prioritized list of valid [PrivateNetworkAddress]es. - */ - @Throws(IOException::class) - fun findAll(): List = - NetworkInterface.getNetworkInterfaces()?.let { ifaces -> - val list: MutableList = mutableListOf() - while (ifaces.hasMoreElements()) { - val iface = ifaces.nextElement() - if (iface.isLoopback || !iface.isUp) { - continue // we don't want loopback interfaces or interfaces that are down - } - list.addAll( - iface.interfaceAddresses.mapNotNull { ifaceAddr -> - val addr = ifaceAddr?.address ?: return@mapNotNull null - if (PrivateNetworkType(addr.address) == null) return@mapNotNull null - ifaceAddr.broadcast?.let { PrivateNetworkAddress(it.hostAddress) } - } - ) - } - list - }.orEmpty() - } -} diff --git a/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkType.kt b/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkType.kt index e5c0b65..a294022 100644 --- a/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkType.kt +++ b/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkType.kt @@ -7,27 +7,32 @@ package com.walkertribe.ian.protocol.udp enum class PrivateNetworkType { TWENTY_FOUR_BIT_BLOCK { // 10.x.x.x + override val broadcastAddress: String get() = TWENTY_FOUR_BIT_BROADCAST override val constraints: Array get() = TWENTY_FOUR_BIT_CONSTRAINTS }, TWENTY_BIT_BLOCK { // 172.16.x.x - 172.31.x.x + override val broadcastAddress: String get() = TWENTY_BIT_BROADCAST override val constraints: Array get() = TWENTY_BIT_CONSTRAINTS }, SIXTEEN_BIT_BLOCK { // 192.168.x.x + override val broadcastAddress: String get() = SIXTEEN_BIT_BROADCAST override val constraints: Array get() = SIXTEEN_BIT_CONSTRAINTS }; - /** - * Returns true if the given address matches this private network type. - */ + abstract val broadcastAddress: String internal abstract val constraints: Array - internal fun match(address: ByteArray): Boolean = address.run { - size == Int.SIZE_BYTES && zip(constraints).all { (byte, cons) -> cons.check(byte) } + internal fun match(address: String): Boolean = address.split('.').run { + size == Int.SIZE_BYTES && zip(constraints).all { (byte, cons) -> cons.check(byte.toByte()) } } companion object { + private const val TWENTY_FOUR_BIT_BROADCAST = "10.255.255.255" + private const val TWENTY_BIT_BROADCAST = "172.31.255.255" + private const val SIXTEEN_BIT_BROADCAST = "192.168.255.255" + private val TWENTY_FOUR_BIT_CONSTRAINTS = arrayOf( ByteConstraint.Equals(10), ) @@ -46,7 +51,6 @@ enum class PrivateNetworkType { * Returns the private network address type that matches the given address, or null if it's * not a private network address. */ - operator fun invoke(address: ByteArray): PrivateNetworkType? = - entries.find { it.match(address) } + fun of(address: String): PrivateNetworkType? = entries.find { it.match(address) } } } diff --git a/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/ServerDiscoveryRequester.kt b/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/ServerDiscoveryRequester.kt index cdd1ae1..1c1b319 100644 --- a/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/ServerDiscoveryRequester.kt +++ b/IAN/udp/src/main/kotlin/com/walkertribe/ian/protocol/udp/ServerDiscoveryRequester.kt @@ -17,12 +17,7 @@ import kotlinx.coroutines.withTimeoutOrNull * continually poll for servers. * @author rjwut */ -class ServerDiscoveryRequester( - internal val broadcastAddress: String = - (PrivateNetworkAddress.guessBest() ?: PrivateNetworkAddress.DEFAULT).hostAddress, - private val listener: Listener, - private val timeoutMs: Long -) { +class ServerDiscoveryRequester(private val listener: Listener, private val timeoutMs: Long) { /** * Interface for an object which is notified when a server is discovered or the discovery * process ends. @@ -39,7 +34,7 @@ class ServerDiscoveryRequester( suspend fun onQuit() } - suspend fun run() { + suspend fun run(broadcastAddress: String) { SelectorManager(Dispatchers.IO).use { selector -> aSocket(selector).udp().bind { broadcast = true @@ -76,6 +71,7 @@ class ServerDiscoveryRequester( companion object { const val PORT = 3100 + const val DEFAULT_BROADCAST_ADDRESS = "255.255.255.255" } init { diff --git a/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkAddressTest.kt b/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkAddressTest.kt deleted file mode 100644 index 430ea90..0000000 --- a/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkAddressTest.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.walkertribe.ian.protocol.udp - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldNotBeEmpty -import io.kotest.matchers.equals.shouldBeEqual -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.property.Arb -import io.kotest.property.arbitrary.byte -import io.kotest.property.arbitrary.byteArray -import io.kotest.property.arbitrary.filter -import io.kotest.property.arbitrary.of -import io.kotest.property.checkAll -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import java.net.InetAddress -import java.net.InterfaceAddress -import java.net.NetworkInterface -import java.util.Enumeration - -class PrivateNetworkAddressTest : DescribeSpec({ - finalizeSpec { unmockkAll() } - - describe("PrivateNetworkAddress") { - it("Default") { - PrivateNetworkAddress.DEFAULT.hostAddress shouldBeEqual "255.255.255.255" - } - - lateinit var validAddress: PrivateNetworkAddress - - it("At least one can be found with a valid broadcast address") { - validAddress = PrivateNetworkAddress.findAll().shouldNotBeEmpty().first() - } - - it("Valid broadcast address is guessed") { - val address = PrivateNetworkAddress.guessBest().shouldNotBeNull() - address.hostAddress shouldBeEqual validAddress.hostAddress - } - - mockkStatic(NetworkInterface::getNetworkInterfaces) - - it("Empty list of addresses if there are no network interfaces") { - every { NetworkInterface.getNetworkInterfaces() } returns null - - PrivateNetworkAddress.findAll().shouldBeEmpty() - } - - it("Does not return loopback address") { - every { - NetworkInterface.getNetworkInterfaces() - } returns mockk> { - every { hasMoreElements() } returnsMany listOf(true, false) - every { nextElement() } returns mockk { - every { isLoopback } returns true - } - } - - PrivateNetworkAddress.findAll().shouldBeEmpty() - } - - it("Does not return address of interface that is down") { - every { - NetworkInterface.getNetworkInterfaces() - } returns mockk> { - every { hasMoreElements() } returnsMany listOf(true, false) - every { nextElement() } returns mockk { - every { isLoopback } returns false - every { isUp } returns false - } - } - - PrivateNetworkAddress.findAll().shouldBeEmpty() - } - - it("Does not return null addresses") { - every { - NetworkInterface.getNetworkInterfaces() - } returns mockk> { - every { hasMoreElements() } returnsMany listOf(true, true, false) - every { nextElement() } returnsMany listOf( - mockk { - every { isLoopback } returns false - every { isUp } returns true - every { interfaceAddresses } returns listOf(null) - }, - mockk { - every { isLoopback } returns false - every { isUp } returns true - every { interfaceAddresses } returns listOf( - mockk { - every { address } returns null - } - ) - } - ) - } - - PrivateNetworkAddress.findAll().shouldBeEmpty() - } - - it("Does not return non-private addresses") { - val validFirstBytes = setOf(10.toByte(), (-44).toByte(), (-64).toByte()) - - Arb.byteArray(Arb.of(4), Arb.byte()).filter { - !validFirstBytes.contains(it[0]) - }.checkAll { bytes -> - every { - NetworkInterface.getNetworkInterfaces() - } returns mockk> { - every { hasMoreElements() } returnsMany listOf(true, false) - every { nextElement() } returns mockk { - every { isLoopback } returns false - every { isUp } returns true - every { interfaceAddresses } returns listOf( - mockk { - every { address } returns mockk { - every { address } returns bytes - } - } - ) - } - } - - PrivateNetworkAddress.findAll().shouldBeEmpty() - } - } - - it("Does not return non-broadcast addresses") { - every { - NetworkInterface.getNetworkInterfaces() - } returns mockk> { - every { hasMoreElements() } returnsMany listOf(true, false) - every { nextElement() } returns mockk { - every { isLoopback } returns false - every { isUp } returns true - every { interfaceAddresses } returns listOf( - mockk { - every { address } returns mockk { - every { address } returns byteArrayOf(10, 0, 0, 0) - } - every { broadcast } returns null - } - ) - } - } - - PrivateNetworkAddress.findAll().shouldBeEmpty() - } - } -}) diff --git a/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkTypeTest.kt b/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkTypeTest.kt index 3d6171e..a89f17d 100644 --- a/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkTypeTest.kt +++ b/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/PrivateNetworkTypeTest.kt @@ -27,6 +27,7 @@ class PrivateNetworkTypeTest : DescribeSpec({ val arbBytes: Arb>, val expectedType: PrivateNetworkType, val expectedMatches: Triple, + val expectedBroadcastAddress: String, ) val allTestSuites = arrayOf( @@ -36,6 +37,7 @@ class PrivateNetworkTypeTest : DescribeSpec({ Arb.triple(Arb.byte(), Arb.byte(), Arb.byte()), PrivateNetworkType.TWENTY_FOUR_BIT_BLOCK, Triple(true, false, false), + "10.255.255.255", ), TestSuite( "20-bit block", @@ -43,6 +45,7 @@ class PrivateNetworkTypeTest : DescribeSpec({ Arb.triple(Arb.byte(min = 16, max = 31), Arb.byte(), Arb.byte()), PrivateNetworkType.TWENTY_BIT_BLOCK, Triple(false, true, false), + "172.31.255.255", ), TestSuite( "16-bit block", @@ -50,6 +53,7 @@ class PrivateNetworkTypeTest : DescribeSpec({ Arb.triple(Arb.of(-88), Arb.byte(), Arb.byte()), PrivateNetworkType.SIXTEEN_BIT_BLOCK, Triple(false, false, true), + "192.168.255.255", ), ) allTestSuites.map { @@ -70,6 +74,7 @@ class PrivateNetworkTypeTest : DescribeSpec({ ) allTestSuites.forEach { suite -> + val first = suite.firstByte describe(suite.testName) { withData( nameFn = { @@ -78,23 +83,27 @@ class PrivateNetworkTypeTest : DescribeSpec({ allTestSuites.zip(suite.expectedMatches.toList()), ) { (testSuite, shouldMatch) -> suite.arbBytes.checkAll { (second, third, fourth) -> - val bytes = byteArrayOf(suite.firstByte, second, third, fourth) - testSuite.expectedType.match(bytes) shouldBeEqual shouldMatch + val address = byteArrayOf(first, second, third, fourth).joinToString(".") + testSuite.expectedType.match(address) shouldBeEqual shouldMatch } } it("Defines correct network type") { suite.arbBytes.checkAll { (second, third, fourth) -> - val bytes = byteArrayOf(suite.firstByte, second, third, fourth) - val networkType = PrivateNetworkType(bytes).shouldNotBeNull() - networkType shouldBeEqual suite.expectedType + val address = byteArrayOf(first, second, third, fourth).joinToString(".") + PrivateNetworkType.of(address).shouldNotBeNull() shouldBeEqual + suite.expectedType } } + it("Broadcast address: ${suite.expectedBroadcastAddress}") { + suite.expectedType.broadcastAddress shouldBeEqual suite.expectedBroadcastAddress + } + describe("Does not match invalid address") { withData(nameFn = { it.first }, invalidTestSuites) { it.second.checkAll { bytes -> - suite.expectedType.match(bytes).shouldBeFalse() + suite.expectedType.match(bytes.joinToString(".")).shouldBeFalse() } } } @@ -104,7 +113,7 @@ class PrivateNetworkTypeTest : DescribeSpec({ describe("Does not exist for invalid address") { withData(nameFn = { it.first }, invalidTestSuites) { it.second.checkAll { bytes -> - PrivateNetworkType(bytes).shouldBeNull() + PrivateNetworkType.of(bytes.joinToString(".")).shouldBeNull() } } } diff --git a/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/ServerDiscoveryRequesterTest.kt b/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/ServerDiscoveryRequesterTest.kt index cc5e595..55e772d 100644 --- a/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/ServerDiscoveryRequesterTest.kt +++ b/IAN/udp/src/test/kotlin/com/walkertribe/ian/protocol/udp/ServerDiscoveryRequesterTest.kt @@ -29,10 +29,6 @@ import io.ktor.network.sockets.aSocket import io.ktor.utils.io.core.buildPacket import io.ktor.utils.io.core.writeFully import io.ktor.utils.io.core.writeText -import io.mockk.clearAllMocks -import io.mockk.every -import io.mockk.mockkObject -import io.mockk.unmockkAll import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -42,11 +38,6 @@ import kotlinx.io.writeShortLe class ServerDiscoveryRequesterTest : DescribeSpec({ failfast = true - afterSpec { - clearAllMocks() - unmockkAll() - } - describe("ServerDiscoveryRequester") { val loopbackAddress = "127.0.0.1" val testServers = Arb.list( @@ -77,20 +68,12 @@ class ServerDiscoveryRequesterTest : DescribeSpec({ it("Throws when initialized with invalid timeout") { Arb.long(max = 0L).checkAll { shouldThrow { - ServerDiscoveryRequester( - broadcastAddress = loopbackAddress, - listener = listener, - timeoutMs = it, - ) + ServerDiscoveryRequester(listener = listener, timeoutMs = it) } } } - val requester = ServerDiscoveryRequester( - broadcastAddress = loopbackAddress, - listener = listener, - timeoutMs = 500L, - ) + val requester = ServerDiscoveryRequester(listener = listener, timeoutMs = 500L) val localAddress = InetSocketAddress(loopbackAddress, ServerDiscoveryRequester.PORT) @@ -102,7 +85,7 @@ class ServerDiscoveryRequesterTest : DescribeSpec({ it("Datagram was sent through UDP") { retry(retryConfig) { requesterJob?.join() - requesterJob = launch { requester.run() } + requesterJob = launch { requester.run(loopbackAddress) } datagram = socket.receive() packet = datagram.packet @@ -121,7 +104,7 @@ class ServerDiscoveryRequesterTest : DescribeSpec({ retry(retryConfig) { discoveredServers.clear() requesterJob?.join() - requesterJob = launch { requester.run() } + requesterJob = launch { requester.run(loopbackAddress) } datagram = socket.receive() testServers.forEach { (ip, hostName) -> @@ -152,7 +135,7 @@ class ServerDiscoveryRequesterTest : DescribeSpec({ retry(retryConfig) { discoveredServers.clear() requesterJob?.join() - requesterJob = launch { requester.run() } + requesterJob = launch { requester.run(loopbackAddress) } datagram = socket.receive() @@ -184,24 +167,5 @@ class ServerDiscoveryRequesterTest : DescribeSpec({ it("Quit after timeout") { onQuitCalled.shouldBeTrue() } - - describe("Constructor") { - it("From discovered broadcast address") { - ServerDiscoveryRequester( - listener = listener, - timeoutMs = 500L, - ) - } - - it("From default broadcast address") { - mockkObject(PrivateNetworkAddress.Companion) - every { PrivateNetworkAddress.guessBest() } returns null - - ServerDiscoveryRequester( - listener = listener, - timeoutMs = 500L, - ).broadcastAddress shouldBeEqual PrivateNetworkAddress.DEFAULT.hostAddress - } - } } }) diff --git a/app/src/androidTest/kotlin/artemis/agent/ArtemisAgentTestHelpers.kt b/app/src/androidTest/kotlin/artemis/agent/ArtemisAgentTestHelpers.kt index 3d1db27..2fb8e19 100644 --- a/app/src/androidTest/kotlin/artemis/agent/ArtemisAgentTestHelpers.kt +++ b/app/src/androidTest/kotlin/artemis/agent/ArtemisAgentTestHelpers.kt @@ -3,6 +3,7 @@ package artemis.agent import androidx.annotation.IdRes import com.adevinta.android.barista.assertion.BaristaCheckedAssertions import com.adevinta.android.barista.assertion.BaristaEnabledAssertions +import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions object ArtemisAgentTestHelpers { fun assertEnabled(@IdRes resId: Int, enabled: Boolean) { @@ -20,4 +21,12 @@ object ArtemisAgentTestHelpers { BaristaCheckedAssertions.assertUnchecked(resId) } } + + fun assertDisplayed(@IdRes resId: Int, displayed: Boolean) { + if (displayed) { + BaristaVisibilityAssertions.assertDisplayed(resId) + } else { + BaristaVisibilityAssertions.assertNotDisplayed(resId) + } + } } diff --git a/app/src/androidTest/kotlin/artemis/agent/setup/ConnectFragmentTest.kt b/app/src/androidTest/kotlin/artemis/agent/setup/ConnectFragmentTest.kt index a8f0a59..39ff298 100644 --- a/app/src/androidTest/kotlin/artemis/agent/setup/ConnectFragmentTest.kt +++ b/app/src/androidTest/kotlin/artemis/agent/setup/ConnectFragmentTest.kt @@ -5,8 +5,10 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import artemis.agent.AgentViewModel +import artemis.agent.ArtemisAgentTestHelpers import artemis.agent.MainActivity import artemis.agent.R +import artemis.agent.setup.settings.SettingsFragmentTest import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertDisabled import com.adevinta.android.barista.assertion.BaristaEnabledAssertions.assertEnabled import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed @@ -19,6 +21,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger @RunWith(AndroidJUnit4::class) @@ -59,4 +62,33 @@ class ConnectFragmentTest { writeTo(R.id.addressBar, "127.0.0.1") assertEnabled(R.id.connectButton) } + + @Test + fun showNetworkInfoTest() { + val showingInfo = AtomicBoolean() + activityScenarioRule.scenario.onActivity { activity -> + showingInfo.lazySet(activity.viewModels().value.showingNetworkInfo) + } + + val infoViews = intArrayOf( + R.id.addressLabel, + R.id.networkTypeLabel, + R.id.networkInfoDivider, + ) + + val settingValue = showingInfo.get() + listOf(settingValue, !settingValue, settingValue).forEachIndexed { index, showing -> + if (index != 0) { + SettingsFragmentTest.openSettingsMenu() + SettingsFragmentTest.openSettingsSubMenu(1) + + clickOn(R.id.showNetworkInfoButton) + clickOn(R.id.connectPageButton) + } + + infoViews.forEach { resId -> + ArtemisAgentTestHelpers.assertDisplayed(resId, showing) + } + } + } } diff --git a/app/src/androidTest/kotlin/artemis/agent/setup/settings/AllySettingsFragmentTest.kt b/app/src/androidTest/kotlin/artemis/agent/setup/settings/AllySettingsFragmentTest.kt index 9a795a0..b665f84 100644 --- a/app/src/androidTest/kotlin/artemis/agent/setup/settings/AllySettingsFragmentTest.kt +++ b/app/src/androidTest/kotlin/artemis/agent/setup/settings/AllySettingsFragmentTest.kt @@ -132,11 +132,7 @@ class AllySettingsFragmentTest { assertNotExist(R.id.allySortingDefaultButton) allySortMethodSettings.forEach { assertNotExist(it.button) } assertNotExist(R.id.allySortingDivider) - allySingleToggleSettings.forEach { - assertNotExist(it.button) - assertNotExist(it.text) - assertNotExist(it.divider) - } + allySingleToggleSettings.forEach { it.testNotExist() } } fun testAllySubMenuSortMethods(sortMethods: BooleanArray, shouldTest: Boolean) { diff --git a/app/src/androidTest/kotlin/artemis/agent/setup/settings/ConnectionSettingsFragmentTest.kt b/app/src/androidTest/kotlin/artemis/agent/setup/settings/ConnectionSettingsFragmentTest.kt index c2b6189..fdd8447 100644 --- a/app/src/androidTest/kotlin/artemis/agent/setup/settings/ConnectionSettingsFragmentTest.kt +++ b/app/src/androidTest/kotlin/artemis/agent/setup/settings/ConnectionSettingsFragmentTest.kt @@ -1,8 +1,10 @@ package artemis.agent.setup.settings +import androidx.activity.viewModels import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import artemis.agent.AgentViewModel import artemis.agent.MainActivity import artemis.agent.R import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed @@ -11,6 +13,7 @@ import com.adevinta.android.barista.interaction.BaristaScrollInteractions.scroll import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.util.concurrent.atomic.AtomicBoolean @RunWith(AndroidJUnit4::class) @LargeTest @@ -20,6 +23,12 @@ class ConnectionSettingsFragmentTest { @Test fun connectionSettingsTest() { + val showingInfo = AtomicBoolean() + + activityScenarioRule.scenario.onActivity { activity -> + showingInfo.lazySet(activity.viewModels().value.showingNetworkInfo) + } + SettingsFragmentTest.openSettingsMenu() SettingsFragmentTest.openSettingsSubMenu(1) @@ -38,6 +47,8 @@ class ConnectionSettingsFragmentTest { assertDisplayed(R.id.scanTimeoutTimeInput) assertDisplayed(R.id.scanTimeoutSecondsLabel, R.string.seconds) + showNetworkInfoSetting.testSingleToggle(showingInfo.get()) + SettingsFragmentTest.closeSettingsSubMenu() assertNotExist(R.id.connectionTimeoutTitle) assertNotExist(R.id.connectionTimeoutTimeInput) @@ -51,5 +62,15 @@ class ConnectionSettingsFragmentTest { assertNotExist(R.id.scanTimeoutTimeInput) assertNotExist(R.id.scanTimeoutSecondsLabel) assertNotExist(R.id.scanTimeoutDivider) + showNetworkInfoSetting.testNotExist() + } + + private companion object { + val showNetworkInfoSetting = SingleToggleButtonSetting( + R.id.showNetworkInfoDivider, + R.id.showNetworkInfoTitle, + R.string.show_network_info, + R.id.showNetworkInfoButton, + ) } } diff --git a/app/src/androidTest/kotlin/artemis/agent/setup/settings/EnemySettingsFragmentTest.kt b/app/src/androidTest/kotlin/artemis/agent/setup/settings/EnemySettingsFragmentTest.kt index 0b5d53f..f219e96 100644 --- a/app/src/androidTest/kotlin/artemis/agent/setup/settings/EnemySettingsFragmentTest.kt +++ b/app/src/androidTest/kotlin/artemis/agent/setup/settings/EnemySettingsFragmentTest.kt @@ -158,11 +158,7 @@ class EnemySettingsFragmentTest { assertNotExist(R.id.surrenderRangeEnableButton) assertNotExist(R.id.surrenderRangeInfinity) assertNotExist(R.id.surrenderRangeDivider) - enemySingleToggleSettings.forEach { - assertNotExist(it.button) - assertNotExist(it.divider) - assertNotExist(it.text) - } + enemySingleToggleSettings.forEach { it.testNotExist() } } fun testEnemySubMenuSortMethods(sortMethods: BooleanArray, shouldTest: Boolean) { diff --git a/app/src/androidTest/kotlin/artemis/agent/setup/settings/SingleToggleButtonSetting.kt b/app/src/androidTest/kotlin/artemis/agent/setup/settings/SingleToggleButtonSetting.kt index ef95390..ee3e2a1 100644 --- a/app/src/androidTest/kotlin/artemis/agent/setup/settings/SingleToggleButtonSetting.kt +++ b/app/src/androidTest/kotlin/artemis/agent/setup/settings/SingleToggleButtonSetting.kt @@ -4,6 +4,7 @@ import androidx.annotation.IdRes import androidx.annotation.StringRes import artemis.agent.ArtemisAgentTestHelpers import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed +import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertNotExist import com.adevinta.android.barista.interaction.BaristaScrollInteractions.scrollTo class SingleToggleButtonSetting( @@ -18,4 +19,10 @@ class SingleToggleButtonSetting( assertDisplayed(button) ArtemisAgentTestHelpers.assertChecked(button, isChecked) } + + fun testNotExist() { + assertNotExist(divider) + assertNotExist(button) + assertNotExist(text) + } } diff --git a/app/src/main/kotlin/artemis/agent/AgentViewModel.kt b/app/src/main/kotlin/artemis/agent/AgentViewModel.kt index 09c2bfa..91a4690 100644 --- a/app/src/main/kotlin/artemis/agent/AgentViewModel.kt +++ b/app/src/main/kotlin/artemis/agent/AgentViewModel.kt @@ -136,6 +136,8 @@ class AgentViewModel(application: Application) : val isScanningUDP: MutableSharedFlow by lazy { MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) } + var showingNetworkInfo: Boolean = true + private set // Saved copy of address bar text in connect fragment var addressBarText: String = "" @@ -806,12 +808,14 @@ class AgentViewModel(application: Application) : /** * Begins scanning for servers via UDP. */ - fun scanForServers() { + fun scanForServers(broadcastAddress: String?) { isScanningUDP.tryEmit(true) discoveredServers.value = listOf() cpu.launch { try { - serverDiscoveryRequester.run() + serverDiscoveryRequester.run( + broadcastAddress ?: ServerDiscoveryRequester.DEFAULT_BROADCAST_ADDRESS + ) } catch (_: Exception) { isScanningUDP.emit(false) } @@ -1446,6 +1450,7 @@ class AgentViewModel(application: Application) : connectTimeout = settings.connectionTimeoutSeconds scanTimeout = settings.scanTimeoutSeconds heartbeatTimeout = settings.serverTimeoutSeconds.toLong() + showingNetworkInfo = settings.showNetworkInfo missionsEnabled = settings.missionsEnabled reconcileDisplayedMissions( @@ -1610,6 +1615,7 @@ class AgentViewModel(application: Application) : threeDigitDirections = this@AgentViewModel.threeDigitDirections soundVolume = (volume * VOLUME_SCALE).toInt() themeValue = ALL_THEMES.indexOf(themeRes) + showNetworkInfo = showingNetworkInfo } companion object { diff --git a/app/src/main/kotlin/artemis/agent/UserSettingsSerializer.kt b/app/src/main/kotlin/artemis/agent/UserSettingsSerializer.kt index 54cb565..66543cc 100644 --- a/app/src/main/kotlin/artemis/agent/UserSettingsSerializer.kt +++ b/app/src/main/kotlin/artemis/agent/UserSettingsSerializer.kt @@ -122,5 +122,7 @@ object UserSettingsSerializer : Serializer theme = UserSettingsOuterClass.UserSettings.Theme.THEME_DEFAULT threeDigitDirections = true soundVolume = DEFAULT_SOUND_VOLUME + + showNetworkInfo = true } } diff --git a/app/src/main/kotlin/artemis/agent/setup/ConnectFragment.kt b/app/src/main/kotlin/artemis/agent/setup/ConnectFragment.kt index 912ca5c..c7aa777 100644 --- a/app/src/main/kotlin/artemis/agent/setup/ConnectFragment.kt +++ b/app/src/main/kotlin/artemis/agent/setup/ConnectFragment.kt @@ -21,6 +21,9 @@ import artemis.agent.databinding.ConnectFragmentBinding import artemis.agent.databinding.fragmentViewBinding import artemis.agent.generic.GenericDataAdapter import artemis.agent.generic.GenericDataEntry +import com.walkertribe.ian.protocol.udp.PrivateNetworkType +import dev.tmapps.konnection.Konnection +import dev.tmapps.konnection.NetworkConnection class ConnectFragment : Fragment(R.layout.connect_fragment) { private val viewModel: AgentViewModel by activityViewModels() @@ -47,8 +50,13 @@ class ConnectFragment : Fragment(R.layout.connect_fragment) { RecentServersAdapter(binding.root.context) } + private val networkTypes: Array by lazy { + binding.root.resources.getStringArray(R.array.network_type_entries) + } + private var playSoundsOnTextChange: Boolean = false private var playSoundOnScanFinished: Boolean = false + private var broadcastAddress: String? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -60,6 +68,7 @@ class ConnectFragment : Fragment(R.layout.connect_fragment) { } } + prepareInfoLabels() prepareConnectionSection() prepareScanningSection() @@ -88,6 +97,25 @@ class ConnectFragment : Fragment(R.layout.connect_fragment) { super.onPause() } + private fun prepareInfoLabels() { + val networkInfoVisibility = if (viewModel.showingNetworkInfo) View.VISIBLE else View.GONE + binding.addressLabel.visibility = networkInfoVisibility + binding.networkTypeLabel.visibility = networkInfoVisibility + binding.networkInfoDivider.visibility = networkInfoVisibility + + viewLifecycleOwner.collectLatestWhileStarted( + Konnection.instance.observeNetworkConnection() + ) { + val info = Konnection.instance.getInfo() + val connection = info?.connection ?: NetworkConnection.UNKNOWN_CONNECTION_TYPE + val address = info?.ipv4 + broadcastAddress = address?.let(PrivateNetworkType::of)?.broadcastAddress + + binding.networkTypeLabel.text = networkTypes[connection.ordinal] + binding.addressLabel.text = address + } + } + private fun prepareConnectionSection() { binding.connectButton.setOnClickListener { hideKeyboard() @@ -151,7 +179,7 @@ class ConnectFragment : Fragment(R.layout.connect_fragment) { scanButton.setOnClickListener { viewModel.playSound(SoundEffect.BEEP_2) hideKeyboard() - viewModel.scanForServers() + viewModel.scanForServers(broadcastAddress) } viewLifecycleOwner.collectLatestWhileStarted(viewModel.discoveredServers) { diff --git a/app/src/main/kotlin/artemis/agent/setup/settings/ConnectionSettingsFragment.kt b/app/src/main/kotlin/artemis/agent/setup/settings/ConnectionSettingsFragment.kt index bd84b90..ca9e2a7 100644 --- a/app/src/main/kotlin/artemis/agent/setup/settings/ConnectionSettingsFragment.kt +++ b/app/src/main/kotlin/artemis/agent/setup/settings/ConnectionSettingsFragment.kt @@ -65,6 +65,15 @@ class ConnectionSettingsFragment : Fragment(R.layout.settings_connection) { connectionTimeoutBinder.timeInSeconds = it.connectionTimeoutSeconds heartbeatTimeoutBinder.timeInSeconds = it.serverTimeoutSeconds scanTimeoutBinder.timeInSeconds = it.scanTimeoutSeconds + binding.showNetworkInfoButton.isChecked = it.showNetworkInfo + } + + binding.showNetworkInfoButton.setOnCheckedChangeListener { _, isChecked -> + viewModel.viewModelScope.launch { + view.context.userSettings.updateData { + it.copy { showNetworkInfo = isChecked } + } + } } } diff --git a/app/src/main/kotlin/artemis/agent/setup/settings/SettingsFragment.kt b/app/src/main/kotlin/artemis/agent/setup/settings/SettingsFragment.kt index 1d335d4..fba0556 100644 --- a/app/src/main/kotlin/artemis/agent/setup/settings/SettingsFragment.kt +++ b/app/src/main/kotlin/artemis/agent/setup/settings/SettingsFragment.kt @@ -56,6 +56,7 @@ class SettingsFragment : Fragment(R.layout.settings_fragment) { connectionTimeoutSeconds = UserSettingsSerializer.DEFAULT_CONNECTION_TIMEOUT serverTimeoutSeconds = UserSettingsSerializer.DEFAULT_HEARTBEAT_TIMEOUT scanTimeoutSeconds = UserSettingsSerializer.DEFAULT_SCAN_TIMEOUT + showNetworkInfo = true } }, MISSION( diff --git a/app/src/main/proto/user_settings.proto b/app/src/main/proto/user_settings.proto index 7089735..c001dfe 100644 --- a/app/src/main/proto/user_settings.proto +++ b/app/src/main/proto/user_settings.proto @@ -85,4 +85,6 @@ message UserSettings { bool show_enemy_intel = 58; bool show_taunt_statuses = 59; bool disable_ineffective_taunts = 60; + + bool show_network_info = 61; } diff --git a/app/src/main/res/layout/connect_fragment.xml b/app/src/main/res/layout/connect_fragment.xml index 01313e7..6cfa24b 100644 --- a/app/src/main/res/layout/connect_fragment.xml +++ b/app/src/main/res/layout/connect_fragment.xml @@ -102,6 +102,31 @@ android:overScrollMode="never" android:scrollbars="none" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toBottomOf="@id/networkInfoDivider" app:layout_constraintTop_toBottomOf="@id/noServersLabel" /> + + + diff --git a/app/src/main/res/layout/settings_connection.xml b/app/src/main/res/layout/settings_connection.xml index b00b687..0217f5f 100644 --- a/app/src/main/res/layout/settings_connection.xml +++ b/app/src/main/res/layout/settings_connection.xml @@ -106,4 +106,33 @@ android:layout_height="1dp" android:background="?android:attr/listDivider" app:layout_constraintTop_toBottomOf="@id/scanTimeoutTimeInput" /> + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96f4250..a173d67 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -544,6 +544,13 @@ Shield Will be removed in %1$ds More commands: + + Wi-Fi + Mobile + Ethernet + Bluetooth tethering + Unknown network + Next: %1$s %2$s %3$s No Scanned enemies will be displayed here. @@ -621,6 +628,7 @@ Ships Show destroyed allies Show intel + Show network info Show taunt status %1$d single-seat craft docked %1$d single-seat craft launched diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d397930..25b9c32 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ gradle-pitest = "1.15.0" junit = "4.13.2" koin = "4.0.0" koin-annotations = "1.4.0" +konnection = "1.4.3" konsist = "0.16.1" korlibs = "6.0.0-alpha2" kotest = "5.9.1" @@ -71,6 +72,9 @@ ksp-koin = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-an ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" } ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor" } +konnection = { module = "dev.tmapps:konnection", version.ref = "konnection" } +konnection-android-debug = { module = "dev.tmapps:konnection-android-debug", version.ref = "konnection" } + korlibs-charset = { module = "com.soywiz:korlibs-charset", version.ref = "korlibs" } korlibs-xml = { module = "com.soywiz:korlibs-serialization-xml", version.ref = "korlibs" } @@ -166,6 +170,7 @@ app = [ "lifecycle-viewmodel", "multidex", "recyclerview", + "konnection", "okio", "okio-assets", "protobuf-javalite", @@ -175,6 +180,7 @@ app = [ ] app-debug = [ "leakcanary", + "konnection-android-debug", ] app-androidTest = [ "barista", @@ -271,7 +277,6 @@ ian-udp-test = [ "kotest-framework-datatest-jvm", "kotest-property", "mockk", - "mockk-dsl", ] ian-util = [