├── test-shared
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── ustadmobile
│ │ └── meshrabiya
│ │ ├── test
│ │ ├── FileAssert.kt
│ │ ├── TemporaryFolderExt.kt
│ │ ├── ByteArrayAssert.kt
│ │ ├── VirtualNodeExt.kt
│ │ ├── FileEchoSocketServer.kt
│ │ ├── TestVirtualNode.kt
│ │ ├── EchoDatagramServer.kt
│ │ └── VirtualPacketTestUtil.kt
│ │ └── FileExt.kt
├── proguard-rules.pro
└── build.gradle
├── lib-meshrabiya
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── mockito-extensions
│ │ │ │ └── org.mockito.plugins.MockMaker
│ │ └── java
│ │ │ └── com
│ │ │ └── ustadmobile
│ │ │ └── meshrabiya
│ │ │ ├── vnet
│ │ │ ├── VirtualNodeSharedTest.kt
│ │ │ ├── VirtualPacketHeaderTest.kt
│ │ │ ├── wifi
│ │ │ │ ├── HotspotResponseTest.kt
│ │ │ │ └── WifiConnectConfigTest.kt
│ │ │ ├── MeshrabiyaConnectLinkTest.kt
│ │ │ ├── VirtualPacketStreamTest.kt
│ │ │ ├── VirtualNodeTest.kt
│ │ │ ├── VirtualNodeDatagramSocketTest.kt
│ │ │ └── VirtualPacketTest.kt
│ │ │ ├── mmcp
│ │ │ ├── MmcpPongTest.kt
│ │ │ ├── MmcpMessageTest.kt
│ │ │ ├── MmcpHotspotResponseTest.kt
│ │ │ └── MmcpOriginatorMessageTest.kt
│ │ │ ├── ext
│ │ │ ├── IntExtTest.kt
│ │ │ └── ByteArrayExtTest.kt
│ │ │ ├── util
│ │ │ └── UuidMaskUtilTest.kt
│ │ │ └── portforward
│ │ │ └── ForwardingTest.kt
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── ustadmobile
│ │ │ │ └── meshrabiya
│ │ │ │ ├── vnet
│ │ │ │ ├── Protocol.kt
│ │ │ │ ├── wifi
│ │ │ │ │ ├── WifiConnectException.kt
│ │ │ │ │ ├── LocalHotspotRequest.kt
│ │ │ │ │ ├── WifiDirectException.kt
│ │ │ │ │ ├── DnsSdResponse.kt
│ │ │ │ │ ├── HotspotStatus.kt
│ │ │ │ │ ├── state
│ │ │ │ │ │ ├── WifiDirectState.kt
│ │ │ │ │ │ ├── LocalOnlyHotspotState.kt
│ │ │ │ │ │ ├── WifiStationState.kt
│ │ │ │ │ │ └── MeshrabiyaWifiState.kt
│ │ │ │ │ ├── WifiConnectEvent.kt
│ │ │ │ │ ├── MeshrabiyaWifiManager.kt
│ │ │ │ │ ├── WifiDirectError.kt
│ │ │ │ │ ├── WifiP2pFailure.kt
│ │ │ │ │ ├── WifiP2pActionListenerAdapter.kt
│ │ │ │ │ ├── ConnectBand.kt
│ │ │ │ │ ├── HotspotType.kt
│ │ │ │ │ ├── HotspotPersistenceType.kt
│ │ │ │ │ └── LocalHotspotResponse.kt
│ │ │ │ ├── WifiRole.kt
│ │ │ │ ├── bluetooth
│ │ │ │ │ ├── MeshrabiyaBluetoothState.kt
│ │ │ │ │ └── MeshrabiyaBluetoothManager.kt
│ │ │ │ ├── PongListener.kt
│ │ │ │ ├── socket
│ │ │ │ │ ├── ChainSocketNextHop.kt
│ │ │ │ │ ├── ChainSocketFactory.kt
│ │ │ │ │ ├── ChainSocketInitResponse.kt
│ │ │ │ │ ├── ChainSocketExt.kt
│ │ │ │ │ ├── ChainSocketInitRequest.kt
│ │ │ │ │ ├── ChainSocket.kt
│ │ │ │ │ └── ChainSocketFactoryImpl.kt
│ │ │ │ ├── ISocket.kt
│ │ │ │ ├── NodeConfig.kt
│ │ │ │ ├── datagram
│ │ │ │ │ ├── VirtualDatagramSocketImplFactory.kt
│ │ │ │ │ └── VirtualDatagramSocket2.kt
│ │ │ │ ├── LocalNodeState.kt
│ │ │ │ ├── BluetoothSocketISocketAdapter.kt
│ │ │ │ ├── VirtualNodeReturnPathSocketFactory.kt
│ │ │ │ ├── VirtualRouter.kt
│ │ │ │ ├── quic
│ │ │ │ │ └── CertGenerator.kt
│ │ │ │ ├── VirtualNodeDatagramSocket.kt
│ │ │ │ ├── MeshrabiyaConnectLink.kt
│ │ │ │ └── VirtualPacketHeader.kt
│ │ │ │ ├── util
│ │ │ │ ├── ByteArrayUtil.kt
│ │ │ │ ├── RandomString.kt
│ │ │ │ ├── FindFreePort.kt
│ │ │ │ ├── FileSerializer.kt
│ │ │ │ ├── InetAddressSerializer.kt
│ │ │ │ └── UuidMaskUtil.kt
│ │ │ │ ├── RemoteEndpoint.kt
│ │ │ │ ├── server
│ │ │ │ └── OnUuidAllocatedListener.kt
│ │ │ │ ├── MeshrabiyaConstants.kt
│ │ │ │ ├── ext
│ │ │ │ ├── WifiConfigurationExt.kt
│ │ │ │ ├── ContextExt.kt
│ │ │ │ ├── LinkPropertiesExt.kt
│ │ │ │ ├── Inet6AddressExt.kt
│ │ │ │ ├── WifiP2pConfigExt.kt
│ │ │ │ ├── KeyPairExt.kt
│ │ │ │ ├── X509CertificateExt.kt
│ │ │ │ ├── CompanionDeviceManagerExt.kt
│ │ │ │ ├── ListExt.kt
│ │ │ │ ├── EnumerationExt.kt
│ │ │ │ ├── IntExt.kt
│ │ │ │ ├── WifiManagerExt.kt
│ │ │ │ ├── WifiP2pGroupExt.kt
│ │ │ │ ├── OutputStreamExt.kt
│ │ │ │ ├── SoftApConfigurationExt.kt
│ │ │ │ ├── ByteArrayExt.kt
│ │ │ │ ├── InetAddressExt.kt
│ │ │ │ └── ByteBufferExt.kt
│ │ │ │ ├── portforward
│ │ │ │ ├── ReturnPathSocketFactory.kt
│ │ │ │ └── ForwardBindPoint.kt
│ │ │ │ ├── mmcp
│ │ │ │ ├── MmcpMessageAndPacketHeader.kt
│ │ │ │ ├── MmcpPing.kt
│ │ │ │ ├── MmcpHeader.kt
│ │ │ │ ├── MmcpPong.kt
│ │ │ │ ├── MmcpHotspotResponse.kt
│ │ │ │ ├── MmcpAck.kt
│ │ │ │ └── MmcpHotspotRequest.kt
│ │ │ │ ├── client
│ │ │ │ └── BluetoothHttpResponse.kt
│ │ │ │ ├── UuidUtil.kt
│ │ │ │ └── log
│ │ │ │ ├── LogLine.kt
│ │ │ │ ├── MNetLogger.kt
│ │ │ │ └── MNetLoggerStdout.kt
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── ustadmobile
│ │ └── httpoverbluetooth
│ │ └── VirtualNodeSharedTest.kt
├── proguard-rules.pro
└── build.gradle
├── test-app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── colors.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── xml
│ │ │ │ └── filepaths.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── ustadmobile
│ │ │ │ └── meshrabiya
│ │ │ │ └── testapp
│ │ │ │ ├── viewmodel
│ │ │ │ ├── SnackbarMessage.kt
│ │ │ │ ├── InfoViewModel.kt
│ │ │ │ ├── SendFileViewModel.kt
│ │ │ │ ├── LogListViewModel.kt
│ │ │ │ ├── ReceiveViewModel.kt
│ │ │ │ ├── NeighborNodeListViewModel.kt
│ │ │ │ └── SelectDestNodeViewModel.kt
│ │ │ │ ├── appstate
│ │ │ │ ├── AppUiState.kt
│ │ │ │ └── FabState.kt
│ │ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Type.kt
│ │ │ │ └── Theme.kt
│ │ │ │ ├── ext
│ │ │ │ ├── ListExt.kt
│ │ │ │ └── ContentResolverExt.kt
│ │ │ │ ├── screens
│ │ │ │ ├── OpenSourceLicensesScreen.kt
│ │ │ │ ├── SendFileScreen.kt
│ │ │ │ ├── SelectDestNodeScreen.kt
│ │ │ │ └── NeighborNodeListScreen.kt
│ │ │ │ ├── ScanQrCodeContract.kt
│ │ │ │ ├── ViewModelFactory.kt
│ │ │ │ ├── server
│ │ │ │ └── InputStreamCounter.kt
│ │ │ │ ├── ContextExt.kt
│ │ │ │ └── MNetLoggerAndroid.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── ustadmobile
│ │ │ └── test_app
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── ustadmobile
│ │ └── test_app
│ │ └── ExampleInstrumentedTest.kt
└── proguard-rules.pro
├── .idea
├── .gitignore
├── compiler.xml
├── kotlinc.xml
├── vcs.xml
├── misc.xml
├── gradle.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── doc
├── mesh.png
└── mesh-image-attrib.txt
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── gradle.properties
└── gradlew.bat
/test-shared/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/lib-meshrabiya/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/lib-meshrabiya/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test-app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
3 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/doc/mesh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/doc/mesh.png
--------------------------------------------------------------------------------
/doc/mesh-image-attrib.txt:
--------------------------------------------------------------------------------
1 | https://pixabay.com/no/illustrations/textura-modell-tekstur-design-3557036/
2 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
2 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Meshrabiya
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/Protocol.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | enum class Protocol {
4 | UDP, TCP
5 | }
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UstadMobile/Meshrabiya/HEAD/test-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/test-shared/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/VirtualNodeSharedTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | class VirtualNodeSharedTest: VirtualNodeIntegrationTest() {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/viewmodel/SnackbarMessage.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.viewmodel
2 |
3 | class SnackbarMessage(
4 | val message: String,
5 | ) {
6 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/WifiConnectException.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | class WifiConnectException(message: String): Exception(message) {
4 | }
--------------------------------------------------------------------------------
/test-app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test-app/src/main/res/xml/filepaths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/util/ByteArrayUtil.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.util
2 |
3 | private val EMPTY_BYTE_ARRAY = ByteArray(0)
4 |
5 | fun emptyByteArray(): ByteArray = EMPTY_BYTE_ARRAY
6 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/RemoteEndpoint.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya
2 |
3 | import java.util.UUID
4 |
5 | data class RemoteEndpoint(
6 | val remoteAddress: String,
7 | val remoteControlUuid: UUID,
8 | )
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/appstate/AppUiState.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.appstate
2 |
3 | data class AppUiState(
4 | val title: String = "",
5 | val fabState: FabState = FabState(),
6 | ) {
7 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/LocalHotspotRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | data class LocalHotspotRequest(
4 | val preferredBand: ConnectBand,
5 | val preferredType: HotspotType,
6 | ) {
7 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/androidTest/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/server/OnUuidAllocatedListener.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.server
2 |
3 | import java.util.UUID
4 |
5 | fun interface OnUuidAllocatedListener {
6 |
7 | operator fun invoke(uuid: UUID)
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/androidTest/java/com/ustadmobile/httpoverbluetooth/VirtualNodeSharedTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.httpoverbluetooth
2 |
3 | import com.ustadmobile.meshrabiya.vnet.VirtualNodeIntegrationTest
4 |
5 | class VirtualNodeSharedTest: VirtualNodeIntegrationTest() {
6 |
7 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jun 04 13:57:33 GST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/WifiRole.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | enum class WifiRole {
4 |
5 | NONE, LOCAL_ONLY_HOTSPOT, WIFI_DIRECT_GROUP_OWNER, CLIENT,
6 |
7 | @Suppress("unused") //Reserved for future use
8 | CLIENT_RELAY,
9 |
10 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/bluetooth/MeshrabiyaBluetoothState.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.bluetooth
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class MeshrabiyaBluetoothState(
7 | val deviceName: String? = null,
8 | ) {
9 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/PongListener.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import com.ustadmobile.meshrabiya.mmcp.MmcpPong
4 |
5 | interface PongListener {
6 |
7 | fun onPongReceived(
8 | fromNode: Int,
9 | pong: MmcpPong,
10 | )
11 |
12 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/WifiDirectException.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | class WifiDirectException(
4 | message: String,
5 | val wifiDirectFailReason: Int
6 | ): Exception(
7 | message + ": " + WifiDirectError(wifiDirectFailReason).toString()
8 | )
9 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/MeshrabiyaConstants.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya
2 |
3 | import java.util.UUID
4 |
5 | object MeshrabiyaConstants {
6 |
7 | const val LOG_TAG = "Meshrabiya"
8 |
9 | const val VERSION = "0.1d11"
10 |
11 | val UUID_BUSY = UUID(0, 0)
12 |
13 |
14 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/DnsSdResponse.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import android.net.wifi.p2p.WifiP2pDevice
4 |
5 | data class DnsSdResponse(
6 | val instanceName: String,
7 | val registrationType: String,
8 | val device: WifiP2pDevice,
9 | )
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | buildconfig.local.properties
17 |
18 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/socket/ChainSocketNextHop.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.socket
2 |
3 | import android.net.Network
4 | import java.net.InetAddress
5 |
6 | data class ChainSocketNextHop(
7 | val address: InetAddress,
8 | val port: Int,
9 | val isFinalDest: Boolean,
10 | val network: Network?,
11 | ) {
12 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/HotspotStatus.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | enum class HotspotStatus {
4 | STARTED, STARTING, STOPPED,
5 |
6 | @Suppress("unused") //Reserved for future use
7 | STOPPING;
8 |
9 | fun isSettled(): Boolean {
10 | return this == STARTED || this == STOPPED
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/appstate/FabState.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.appstate
2 |
3 | import androidx.compose.ui.graphics.vector.ImageVector
4 |
5 | data class FabState(
6 | val visible: Boolean = false,
7 | val label: String? = null,
8 | val icon: ImageVector? = null,
9 | val onClick: () -> Unit = { },
10 | ) {
11 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/WifiConfigurationExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import android.net.wifi.WifiConfiguration
4 |
5 | @Suppress("Deprecation") //Must use WiFiconfiguration to support pre SDK30 devices
6 | fun WifiConfiguration.prettyPrint(): String {
7 | return "WifiConfiguratino(ssid=$SSID passphrase=$preSharedKey BSSID=$BSSID)"
8 | }
9 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/portforward/ReturnPathSocketFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.portforward
2 |
3 | import java.net.DatagramSocket
4 | import java.net.InetAddress
5 |
6 | fun interface ReturnPathSocketFactory {
7 |
8 | fun createSocket(
9 | destAddress: InetAddress,
10 | port: Int,
11 | ): DatagramSocket
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/ISocket.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import java.io.InputStream
4 | import java.io.OutputStream
5 |
6 | /**
7 | * Common interface that can represent any socket e.g. Bluetooth, IP, etc.
8 | */
9 | interface ISocket {
10 |
11 | val inStream: InputStream
12 |
13 | val outputStream: OutputStream
14 |
15 | fun close()
16 |
17 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.preferencesDataStore
7 |
8 | val Context.bssidDataStore: DataStore by preferencesDataStore(name = "bssids")
9 |
10 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/NodeConfig.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | data class NodeConfig(
4 | val maxHops: Int,
5 | val originatingMessageInterval: Long = 3000,
6 | val originatingMessageInitialDelay: Long = 1000,
7 | ) {
8 | companion object {
9 | val DEFAULT_CONFIG = NodeConfig(
10 | maxHops = 5,
11 | )
12 | }
13 |
14 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/state/WifiDirectState.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi.state
2 |
3 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
4 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotStatus
5 |
6 | data class WifiDirectState(
7 | val hotspotStatus: HotspotStatus = HotspotStatus.STOPPED,
8 | val error: Int = 0,
9 | val config: WifiConnectConfig? = null,
10 | ) {
11 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/LinkPropertiesExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import android.net.LinkProperties
4 | import android.os.Build
5 |
6 | fun LinkProperties.toPrettyString(): String {
7 | val dhcpServerStr = if(Build.VERSION.SDK_INT >= 30) {
8 | dhcpServerAddress.toString()
9 | }else {
10 | ""
11 | }
12 |
13 | return "LinkProperties: dhcpServer=$dhcpServerStr "
14 | }
15 |
--------------------------------------------------------------------------------
/test-app/src/test/java/com/ustadmobile/test_app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.test_app
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/test/FileAssert.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.test
2 |
3 | import com.ustadmobile.meshrabiya.md5sum
4 | import org.junit.Assert
5 | import java.io.File
6 |
7 | fun assertFileContentsAreEqual(
8 | expected: File,
9 | actual: File,
10 | ) {
11 | Assert.assertEquals(expected.length(), actual.length())
12 | val expectedMd5 = expected.md5sum()
13 | assertByteArrayEquals(expectedMd5, 0, actual.md5sum(), 0, expectedMd5.size)
14 | }
15 |
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/test/TemporaryFolderExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.test
2 |
3 | import com.ustadmobile.meshrabiya.writeRandomData
4 | import org.junit.rules.TemporaryFolder
5 | import java.io.File
6 |
7 | fun TemporaryFolder.newFileWithRandomData(size: Int, name: String? = null): File {
8 | val file = if(name != null){
9 | newFile(name)
10 | }else {
11 | newFile()
12 | }
13 |
14 | file.writeRandomData(size)
15 |
16 | return file
17 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/WifiConnectEvent.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import com.ustadmobile.meshrabiya.vnet.VirtualNodeDatagramSocket
4 | import java.net.InetAddress
5 |
6 | /**
7 | * Event triggered by the MeshrabiyaWifiManager when a new connection
8 | */
9 | data class WifiConnectEvent(
10 | val neighborPort: Int,
11 | val neighborInetAddress: InetAddress,
12 | val socket: VirtualNodeDatagramSocket,
13 | val neighborVirtualAddress: Int,
14 | )
15 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/ext/ListExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.ext
2 |
3 | inline fun List.updateItem(
4 | updatePredicate: (T) -> Boolean,
5 | function: (T) -> T,
6 | ): List {
7 | val indexToUpdate = indexOfFirst(updatePredicate)
8 | return if(indexToUpdate == -1) {
9 | this
10 | }else {
11 | toMutableList().also {newList ->
12 | newList[indexToUpdate] = function(this[indexToUpdate])
13 | }.toList()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/util/RandomString.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.util
2 |
3 | import kotlin.random.Random
4 |
5 | private val CHAR_POOL_DEFAULT = "abcdefghikjmnpqrstuvxwyz23456789"
6 |
7 |
8 | /**
9 | * Generate a random string (e.g. default password, class code, etc.
10 | */
11 | fun randomString(length: Int, charPool: String = CHAR_POOL_DEFAULT): String {
12 | return (1 .. length).map { i -> charPool.get(Random.nextInt(0, charPool.length)) }
13 | .joinToString(separator = "")
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/datagram/VirtualDatagramSocketImplFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.datagram
2 |
3 | import com.ustadmobile.meshrabiya.vnet.VirtualNode
4 | import java.net.DatagramSocketImpl
5 | import java.net.DatagramSocketImplFactory
6 |
7 | class VirtualDatagramSocketImplFactory(
8 | private val node: VirtualNode,
9 | ): DatagramSocketImplFactory {
10 |
11 | override fun createDatagramSocketImpl(): DatagramSocketImpl {
12 |
13 | TODO("Not yet implemented")
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/mmcp/MmcpMessageAndPacketHeader.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import com.ustadmobile.meshrabiya.vnet.VirtualPacketHeader
4 |
5 | /**
6 | * Contains the MmcpMessage as received and the packet header (e.g. from/to values etc). The packet
7 | * header is immutable and therefor won't be affected by any other usage of the underlying data
8 | * buffer.
9 | */
10 | data class MmcpMessageAndPacketHeader(
11 | val message: MmcpMessage,
12 | val packetHeader: VirtualPacketHeader,
13 | )
14 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/client/BluetoothHttpResponse.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.client
2 |
3 | import rawhttp.core.RawHttpResponse
4 | import java.io.Closeable
5 |
6 | /**
7 | * BluetoothHttpResponse containing the RawHttpResponse itself. This MUST be closed when used to
8 | * release the underlying socket.
9 | */
10 | class BluetoothHttpResponse(
11 | val response: RawHttpResponse<*>,
12 | internal val onClose: () -> Unit,
13 | ) : Closeable{
14 |
15 | override fun close() {
16 | onClose()
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/Inet6AddressExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import java.net.Inet6Address
4 | import java.net.InetAddress
5 |
6 | /**
7 | * Get the IPv6 address without any interface scope ( without @interfacename )
8 | */
9 | fun Inet6Address.withoutScope(): Inet6Address {
10 | return Inet6Address.getByAddress(this.address) as Inet6Address
11 | }
12 |
13 | fun InetAddress.requireHostAddress(): String {
14 | return hostAddress ?: throw IllegalStateException("No host address on address $this")
15 | }
16 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/mmcp/MmcpPongTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 |
6 | class MmcpPongTest {
7 |
8 | @Test
9 | fun givenPongMessage_whenConvertedToFromBytes_thenShouldMatch() {
10 | val pong = MmcpPong(42, 4042)
11 | val bytes = pong.toBytes()
12 | val fromBytes = MmcpPong.fromBytes(bytes)
13 | Assert.assertEquals(pong.messageId, fromBytes.messageId)
14 | Assert.assertEquals(pong.replyToMessageId, fromBytes.replyToMessageId)
15 | }
16 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/WifiP2pConfigExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import android.net.wifi.p2p.WifiP2pConfig
4 | import android.os.Build
5 | import com.ustadmobile.meshrabiya.vnet.wifi.ConnectBand
6 |
7 | fun WifiP2pConfig.prettyPrint(): String {
8 | return if(Build.VERSION.SDK_INT >= 30) {
9 | "WifiP2pConfig (networkName=${networkName} networkId=$networkId passphrase=$passphrase " +
10 | "groupOwnerBand=${ConnectBand.fromFlag(groupOwnerBand.toByte())})"
11 | }else {
12 | toString()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | mavenLocal()
14 | maven { url 'https://jitpack.io' }
15 | maven { url "https://devserver3.ustadmobile.com/maven2/" }
16 | }
17 | }
18 | rootProject.name = "Meshrabiya"
19 | include ':lib-meshrabiya'
20 | include ':test-app'
21 | include ':test-shared'
22 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/KeyPairExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import org.bouncycastle.util.io.pem.PemObject
4 | import org.bouncycastle.util.io.pem.PemWriter
5 | import java.io.PrintWriter
6 | import java.io.StringWriter
7 | import java.security.PrivateKey
8 |
9 | fun PrivateKey.toPem(): String {
10 |
11 | val stringWriter = StringWriter()
12 | val pemWriter = PemWriter(PrintWriter(stringWriter))
13 | pemWriter.writeObject {
14 | PemObject("PRIVATE KEY", encoded)
15 | }
16 | pemWriter.flush()
17 |
18 | return stringWriter.toString()
19 | }
20 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/bluetooth/MeshrabiyaBluetoothManager.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.bluetooth
2 |
3 | import android.bluetooth.BluetoothAdapter
4 | import android.bluetooth.BluetoothManager
5 | import android.content.Context
6 |
7 | class MeshrabiyaBluetoothManager(
8 | private val appContext: Context,
9 | ) {
10 |
11 | private val bluetoothManager: BluetoothManager = appContext.getSystemService(
12 | BluetoothManager::class.java
13 | )
14 |
15 | private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
16 |
17 |
18 |
19 |
20 |
21 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/portforward/ForwardBindPoint.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.portforward
2 |
3 | import com.ustadmobile.meshrabiya.vnet.VirtualNode
4 | import java.net.InetAddress
5 |
6 | /**
7 | * Represents a binding point for a forwarding rule. Forwarding can be bound to a specific address
8 | * (e.g. the loopback address) or it can be bound to a zone - the virtual network zone or the real
9 | * network zone.
10 | */
11 | internal data class ForwardBindPoint(
12 | val listenInterface: InetAddress?,
13 | val listenZone: VirtualNode.Zone?,
14 | val listenPort: Int,
15 | ) {
16 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/LocalNodeState.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import com.ustadmobile.meshrabiya.vnet.bluetooth.MeshrabiyaBluetoothState
4 | import com.ustadmobile.meshrabiya.vnet.wifi.state.MeshrabiyaWifiState
5 |
6 | data class LocalNodeState(
7 | val address: Int = 0,
8 | val wifiState: MeshrabiyaWifiState = MeshrabiyaWifiState(),
9 | val bluetoothState: MeshrabiyaBluetoothState = MeshrabiyaBluetoothState(deviceName = ""),
10 | val connectUri: String? = null,
11 | val originatorMessages: Map = emptyMap(),
12 | ) {
13 | }
14 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/X509CertificateExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import org.bouncycastle.util.io.pem.PemObject
4 | import org.bouncycastle.util.io.pem.PemWriter
5 | import java.io.PrintWriter
6 | import java.io.StringWriter
7 | import java.security.cert.X509Certificate
8 |
9 | fun X509Certificate.toPem(): String {
10 | val stringWriter = StringWriter()
11 | val pemWriter = PemWriter(PrintWriter(stringWriter))
12 | pemWriter.writeObject {
13 | PemObject("CERTIFICATE", this.encoded)
14 | }
15 | pemWriter.flush()
16 |
17 |
18 | return stringWriter.toString()
19 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/screens/OpenSourceLicensesScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.screens
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import com.google.accompanist.web.WebView
7 | import com.google.accompanist.web.rememberWebViewState
8 |
9 | @Composable
10 | fun OpenSourceLicensesScreen(){
11 | val state = rememberWebViewState("file:///android_asset/open_source_licenses.html")
12 |
13 | WebView(
14 | modifier = Modifier.fillMaxSize(),
15 | state = state
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/CompanionDeviceManagerExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import android.companion.CompanionDeviceManager
4 | import android.net.MacAddress
5 | import android.os.Build
6 |
7 |
8 | @Suppress("DEPRECATION") //Must use deprecated .assocations to support pre-SDK33
9 | fun CompanionDeviceManager.isAssociatedWithCompat(
10 | bssid: String
11 | ) : Boolean {
12 | return if(Build.VERSION.SDK_INT >= 33) {
13 | val knownAdd = MacAddress.fromString(bssid)
14 | myAssociations.any { it.deviceMacAddress == knownAdd }
15 | }else {
16 | bssid in this.associations
17 | }
18 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/UuidUtil.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya
2 |
3 | import java.nio.ByteBuffer
4 | import java.util.UUID
5 |
6 | fun UUID.toBytes(): ByteArray {
7 | val byteBuffer = ByteBuffer.wrap(ByteArray(16))
8 | byteBuffer.putLong(mostSignificantBits)
9 | byteBuffer.putLong(leastSignificantBits)
10 | return byteBuffer.array()
11 | }
12 |
13 | object UuidUtil {
14 |
15 | fun uuidFromBytes(bytes: ByteArray): UUID {
16 | val byteBuffer = ByteBuffer.wrap(bytes)
17 | val mostSigBits = byteBuffer.long
18 | val leastSigBits = byteBuffer.long
19 | return UUID(mostSigBits, leastSigBits)
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/util/FindFreePort.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.util
2 |
3 | import java.net.DatagramSocket
4 | import java.net.ServerSocket
5 | import kotlin.random.Random
6 |
7 |
8 | fun findFreePort(preferred: Int = 0): Int {
9 | var portToTry = if(preferred == 0) Random.nextInt(1025, 65_536) else preferred
10 | while(true){
11 | try {
12 | ServerSocket(portToTry).close()
13 | DatagramSocket(portToTry).close()
14 | return portToTry
15 | }catch(e: Exception) {
16 | //Do nothing - try another port
17 | }
18 | portToTry = Random.nextInt(1025, 65_536)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/datagram/VirtualDatagramSocket2.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.datagram
2 |
3 | import com.ustadmobile.meshrabiya.log.MNetLogger
4 | import com.ustadmobile.meshrabiya.vnet.VirtualRouter
5 | import java.net.DatagramSocket
6 |
7 | /**
8 | * Thin wrapper required so that we can access the protected constructor specifying the impl class
9 | */
10 | class VirtualDatagramSocket2(
11 | router: VirtualRouter,
12 | localVirtualAddress: Int,
13 | logger: MNetLogger,
14 | ): DatagramSocket(VirtualDatagramSocketImpl(
15 | router = router,
16 | localVirtualAddress = localVirtualAddress,
17 | logger = logger,
18 | ))
19 |
20 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/ext/IntExtTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 | import java.net.InetAddress
6 |
7 | class IntExtTest {
8 |
9 | @Test
10 | fun givenAddr_whenConvertedToFromInt_thenShouldMatch() {
11 | val inetAddress = InetAddress.getByName("192.168.49.1")
12 | val addressToInt = inetAddress.address.ip4AddressToInt()
13 | val inetAddressFromInt = InetAddress.getByAddress(addressToInt.addressToByteArray())
14 | Assert.assertEquals(inetAddress, inetAddressFromInt)
15 | Assert.assertEquals("192.168.49.1", addressToInt.addressToDotNotation())
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/util/UuidMaskUtilTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.util
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 | import java.util.UUID
6 |
7 | class UuidMaskUtilTest {
8 |
9 | @Test
10 | fun givenUuidMaskAndPort_whenMaskedAndPortExtracted_thenShouldMatch() {
11 | val uuidMask = UUID.randomUUID()
12 | val port = 50000
13 | val uuidForMaskAndPort = uuidForMaskAndPort(uuidMask, port)
14 |
15 | Assert.assertEquals(port, uuidForMaskAndPort.maskedPort())
16 | Assert.assertTrue(uuidForMaskAndPort.matchesMask(uuidMask))
17 | Assert.assertFalse(UUID.randomUUID().matchesMask(uuidMask))
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/MeshrabiyaWifiManager.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import com.ustadmobile.meshrabiya.vnet.wifi.state.MeshrabiyaWifiState
4 | import kotlinx.coroutines.flow.Flow
5 |
6 |
7 | interface MeshrabiyaWifiManager {
8 |
9 | val state: Flow
10 |
11 | val is5GhzSupported: Boolean
12 |
13 | suspend fun requestHotspot(
14 | requestMessageId: Int,
15 | request: LocalHotspotRequest
16 | ): LocalHotspotResponse
17 |
18 | suspend fun deactivateHotspot()
19 |
20 |
21 | suspend fun connectToHotspot(
22 | config: WifiConnectConfig,
23 | timeout: Long = 90_000,
24 | )
25 |
26 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/ListExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | inline fun List.appendOrReplace(
4 | item: T,
5 | replace: (T) -> Boolean
6 | ): List {
7 | val indexOfItemToReplace = indexOfFirst(replace)
8 | return if(indexOfItemToReplace == -1) {
9 | buildList {
10 | addAll(this@appendOrReplace)
11 | add(item)
12 | }
13 | }else {
14 | toMutableList().also {
15 | it[indexOfItemToReplace] = item
16 | }
17 | }
18 | }
19 |
20 | fun List.trimIfExceeds(numItems: Int): List {
21 | return if(size > numItems)
22 | subList(0, numItems)
23 | else
24 | this
25 | }
26 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/log/LogLine.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.log
2 |
3 | import com.ustadmobile.meshrabiya.log.MNetLogger.Companion.priorityLabel
4 | import java.util.concurrent.atomic.AtomicInteger
5 |
6 | val LOG_LINE_ID_ATOMIC = AtomicInteger(0)
7 |
8 | data class LogLine(
9 | val line: String,
10 | val priority: Int,
11 | val time: Long,
12 | val lineId: Int = LOG_LINE_ID_ATOMIC.getAndIncrement(),
13 | ) {
14 |
15 | fun toString(epochTime: Long): String {
16 | val time = (time - epochTime) / 1000.toFloat()
17 | val rounded = (time * 100).toInt() / 100.toFloat()
18 |
19 | return "${priorityLabel(priority)}: t+${rounded}s : $line"
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/BluetoothSocketISocketAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import android.bluetooth.BluetoothSocket
4 | import java.io.InputStream
5 | import java.io.OutputStream
6 |
7 | class BluetoothSocketISocketAdapter(
8 | private val bluetoothSocket: BluetoothSocket
9 | ): ISocket {
10 |
11 | override val inStream: InputStream
12 | get() = bluetoothSocket.inputStream
13 |
14 | override val outputStream: OutputStream
15 | get() = bluetoothSocket.outputStream
16 |
17 | override fun close() {
18 | bluetoothSocket.close()
19 | }
20 |
21 | }
22 |
23 | fun BluetoothSocket.asISocket(): ISocket {
24 | return BluetoothSocketISocketAdapter(this)
25 | }
26 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/log/MNetLogger.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.log
2 |
3 | import android.util.Log
4 |
5 | abstract class MNetLogger {
6 |
7 | abstract operator fun invoke(priority: Int, message: String, exception: Exception? = null)
8 |
9 | abstract operator fun invoke(priority: Int, message: () -> String, exception: Exception? = null)
10 |
11 | companion object {
12 |
13 | fun priorityLabel(priority: Int) = when(priority) {
14 | Log.DEBUG -> "D"
15 | Log.ERROR -> "E"
16 | Log.WARN -> "W"
17 | Log.ASSERT -> "A"
18 | Log.VERBOSE -> "V"
19 | Log.INFO -> "I"
20 | else -> "Err-Priority-unknown"
21 | }
22 |
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/ScanQrCodeContract.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.activity.result.contract.ActivityResultContract
7 |
8 | class ScanQrCodeContract: ActivityResultContract() {
9 |
10 | override fun createIntent(context: Context, input: Unit): Intent {
11 | return Intent(context, CodeScannerActivity::class.java)
12 | }
13 |
14 | override fun parseResult(resultCode: Int, intent: Intent?): String? {
15 | return if(resultCode == Activity.RESULT_OK) {
16 | intent?.getStringExtra(CodeScannerActivity.KEY_QR_TEXT)
17 | }else {
18 | null
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/mmcp/MmcpPing.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import com.ustadmobile.meshrabiya.util.emptyByteArray
4 |
5 | class MmcpPing(
6 | messageId: Int
7 | ): MmcpMessage(WHAT_PING, messageId) {
8 |
9 | override fun toBytes() = headerAndPayloadToBytes(header, emptyByteArray())
10 |
11 | //There is no need for equals/hashcode here because there is no real payload
12 |
13 |
14 | companion object {
15 |
16 | fun fromBytes(
17 | byteArray: ByteArray,
18 | offset: Int = 0,
19 | len: Int = byteArray.size,
20 | ): MmcpPing {
21 | val (header, _) = mmcpHeaderAndPayloadFromBytes(byteArray, offset, len)
22 | return MmcpPing(header.messageId)
23 | }
24 |
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/socket/ChainSocketFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.socket
2 |
3 | import java.net.InetAddress
4 | import java.net.Socket
5 | import javax.net.SocketFactory
6 |
7 | /**
8 | * Chain Socket Factory provides a SocketFactory that can connect to addresses on the virtual network.
9 | * See concept notes in ChainSocketServer.
10 | *
11 | * @param virtualRouter local node virtual router
12 | * @param systemSocketFactory the underlying system socket factory
13 | */
14 | abstract class ChainSocketFactory: SocketFactory() {
15 |
16 | data class ChainSocketResult(
17 | val socket: Socket,
18 | val nextHop: ChainSocketNextHop,
19 | )
20 |
21 | abstract fun createChainSocket(address: InetAddress, port: Int): ChainSocketResult
22 |
23 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/ext/ByteArrayExtTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 |
6 | class ByteArrayExtTest {
7 |
8 | @Test
9 | fun givenTwoAddresses_whenCheckPrefixMatches_thenShouldCalculateCorrectly() {
10 | val addr1 = byteArrayOf(169.toByte(), 254.toByte(), 1.toByte(), 1.toByte())
11 | val addr2 = byteArrayOf(169.toByte(), 254.toByte(), 128.toByte(), 64.toByte())
12 |
13 | Assert.assertTrue(addr1.prefixMatches(16, addr2))
14 | Assert.assertTrue(addr1.prefixMatches(8, addr2))
15 | Assert.assertTrue(addr1.prefixMatches(12, addr2))
16 |
17 | Assert.assertFalse(addr1.prefixMatches(24, addr2))
18 | Assert.assertFalse(addr1.prefixMatches(17, addr2))
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/EnumerationExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import java.util.Enumeration
4 |
5 |
6 | /**
7 | * Same as firstNotNullOfOrNull as per Kotlin Collections extensions
8 | */
9 | inline fun Enumeration.firstNotNullOfOrNull(
10 | transform: (T) -> R?
11 | ): R? {
12 | while(hasMoreElements()) {
13 | val transformed = transform(nextElement())
14 | if(transformed != null)
15 | return transformed
16 | }
17 |
18 | return null
19 | }
20 |
21 | fun Enumeration.firstOrNull(
22 | predicate: (T) -> Boolean
23 | ): T? {
24 | while(hasMoreElements()) {
25 | val element = nextElement()
26 | if(predicate(element))
27 | return element
28 | }
29 |
30 | return null
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/test-app/src/androidTest/java/com/ustadmobile/test_app/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.test_app
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.ustadmobile.test_app", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/test-shared/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/lib-meshrabiya/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/mmcp/MmcpMessageTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 | import kotlin.random.Random
6 |
7 | class MmcpMessageTest {
8 |
9 | @Test
10 | fun givenPingMessage_whenConvertedToAndFromVirtualPacket_thenWillMatch() {
11 | val pingMessage = MmcpPing(Random.nextInt())
12 | val pingPacket = pingMessage.toVirtualPacket(
13 | toAddr = 1000,
14 | fromAddr = 1042,
15 | )
16 |
17 | val pingFromPacket = MmcpMessage.fromVirtualPacket(pingPacket) as MmcpPing
18 |
19 | Assert.assertEquals(pingMessage.messageId, pingFromPacket.messageId)
20 | Assert.assertEquals(1000, pingPacket.header.toAddr)
21 | Assert.assertEquals(1042, pingPacket.header.fromAddr)
22 |
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp
2 |
3 | import android.os.Bundle
4 | import androidx.lifecycle.AbstractSavedStateViewModelFactory
5 | import androidx.lifecycle.SavedStateHandle
6 | import androidx.lifecycle.ViewModel
7 | import androidx.savedstate.SavedStateRegistryOwner
8 | import org.kodein.di.DI
9 |
10 | class ViewModelFactory(
11 | private val di: DI,
12 | owner: SavedStateRegistryOwner,
13 | defaultArgs: Bundle?,
14 | private val vmFactory: (DI) -> T,
15 | ): AbstractSavedStateViewModelFactory(owner, defaultArgs) {
16 |
17 | @Suppress("UNCHECKED_CAST")
18 | override fun create(
19 | key: String,
20 | modelClass: Class,
21 | handle: SavedStateHandle
22 | ): T {
23 | return vmFactory(di) as T
24 | }
25 | }
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/test/ByteArrayAssert.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.test
2 |
3 | import org.junit.Assert
4 |
5 | fun assertByteArrayEquals(
6 | expected: ByteArray,
7 | expectedOffset: Int,
8 | actual: ByteArray,
9 | actualOffset: Int,
10 | length: Int,
11 | ) {
12 | for(i in 0 until length) {
13 | Assert.assertEquals("ByteArray expected[$i + $expectedOffset] == actual[$i + $actualOffset]",
14 | expected[expectedOffset + i], actual[actualOffset + i])
15 | }
16 | }
17 |
18 | fun ByteArray.contentRangeEqual(
19 | thisOffset: Int,
20 | other: ByteArray,
21 | otherOffset: Int,
22 | length: Int
23 | ) : Boolean {
24 | for(i in 0 until length) {
25 | if(this[i + thisOffset] != other[i + otherOffset])
26 | return false
27 | }
28 |
29 | return true
30 | }
31 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/WifiDirectError.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import android.net.wifi.p2p.WifiP2pManager
4 |
5 | class WifiDirectError(private val errorCode: Int) {
6 |
7 | override fun toString(): String {
8 | return errorString(errorCode)
9 | }
10 |
11 | companion object {
12 |
13 | /**
14 | * Possible errors as per https://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.ActionListener
15 | */
16 | fun errorString(reason: Int) : String {
17 | return when(reason) {
18 | WifiP2pManager.P2P_UNSUPPORTED -> "P2P_UNSUPPORTED"
19 | WifiP2pManager.BUSY -> "BUSY"
20 | WifiP2pManager.ERROR -> "ERROR"
21 | else -> "Unknown reason: $reason"
22 | }
23 | }
24 |
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/VirtualPacketHeaderTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 |
6 | class VirtualPacketHeaderTest {
7 |
8 | @Test
9 | fun givenHeaderObject_whenToBytesThenFromBytesCalled_thenShouldBeEqual() {
10 | val header = VirtualPacketHeader(
11 | toAddr = 1000,
12 | toPort = 8080,
13 | fromAddr = 1002,
14 | fromPort = 8072,
15 | lastHopAddr = 1002,
16 | hopCount = 1,
17 | maxHops = 4,
18 | payloadSize = 1300
19 | )
20 |
21 | val headerInBytes = header.toBytes()
22 |
23 | val headerFromBytes = VirtualPacketHeader.fromBytes(headerInBytes)
24 |
25 | Assert.assertEquals("Header matches when serialized/deserialized", header, headerFromBytes)
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/IntExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import java.net.InetAddress
4 | import java.nio.ByteBuffer
5 | import java.nio.ByteOrder
6 |
7 |
8 | fun Int.addressToDotNotation() : String {
9 | return "${(this shr 24).and(0xff)}.${(this shr 16).and(0xff)}" +
10 | ".${(this shr 8).and(0xff)}.${this.and(0xff)}"
11 | }
12 |
13 | fun Int.addressToByteArray(): ByteArray {
14 | return ByteBuffer.wrap(ByteArray(4)).order(ByteOrder.BIG_ENDIAN).putInt(this).array()
15 | }
16 |
17 | @OptIn(ExperimentalUnsignedTypes::class)
18 | fun Int.encodeAsHex(): String {
19 | return addressToByteArray().asUByteArray().joinToString(separator = "") {
20 | it.toString(radix = 16).padStart(2, '0')
21 | }
22 | }
23 |
24 | fun Int.asInetAddress(): InetAddress {
25 | return InetAddress.getByAddress(addressToByteArray())
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/util/FileSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.util
2 |
3 | import kotlinx.serialization.KSerializer
4 | import kotlinx.serialization.descriptors.PrimitiveKind
5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
6 | import kotlinx.serialization.descriptors.SerialDescriptor
7 | import kotlinx.serialization.encoding.Decoder
8 | import kotlinx.serialization.encoding.Encoder
9 | import java.io.File
10 |
11 |
12 | object FileSerializer: KSerializer {
13 | override fun deserialize(decoder: Decoder): File {
14 | return File(decoder.decodeString())
15 | }
16 |
17 | override val descriptor: SerialDescriptor
18 | get() = PrimitiveSerialDescriptor("file", PrimitiveKind.STRING)
19 |
20 | override fun serialize(encoder: Encoder, value: File) {
21 | encoder.encodeString(value.absolutePath)
22 | }
23 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/WifiP2pFailure.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import android.net.wifi.p2p.WifiP2pManager
4 |
5 | enum class WifiP2pFailure(val reason: Int) {
6 |
7 | //Reasons as per
8 | // https://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.ActionListener#onFailure(int)
9 |
10 | P2P_UNSUPPORTED(WifiP2pManager.P2P_UNSUPPORTED),
11 | ERROR(WifiP2pManager.ERROR),
12 | BUSY(WifiP2pManager.BUSY),
13 | OTHER(0);
14 |
15 | companion object {
16 |
17 | fun valueOf(reason: Int): WifiP2pFailure {
18 | return values().firstOrNull { it.reason == reason } ?: OTHER
19 | }
20 |
21 | fun reasonToString(reason: Int): String {
22 | return values().firstOrNull { it.reason == reason }?.name
23 | ?: "Unknown: reason=$reason"
24 | }
25 |
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/socket/ChainSocketInitResponse.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.socket
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 |
6 | data class ChainSocketInitResponse(
7 | val statusCode: Int,
8 | ) {
9 |
10 | fun toBytes(): ByteArray {
11 | return ByteArray(4).also {
12 | ByteBuffer.wrap(it)
13 | .order(ByteOrder.BIG_ENDIAN)
14 | .putInt(statusCode)
15 | }
16 | }
17 |
18 | companion object {
19 |
20 | const val MESSAGE_SIZE = 4
21 |
22 | fun fromBytes(byteArray: ByteArray, offset: Int): ChainSocketInitResponse{
23 | val byteBuf = ByteBuffer.wrap(byteArray, offset, MESSAGE_SIZE)
24 | .order(ByteOrder.BIG_ENDIAN)
25 | val statusCode = byteBuf.int
26 | return ChainSocketInitResponse(statusCode)
27 | }
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/WifiManagerExt.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION") //Must use deprecated classes to support pre-SDK29 devices
2 |
3 | package com.ustadmobile.meshrabiya.ext
4 |
5 | import android.annotation.SuppressLint
6 | import android.net.wifi.WifiConfiguration
7 | import android.net.wifi.WifiManager
8 | import android.util.Log
9 | import com.ustadmobile.meshrabiya.log.MNetLogger
10 |
11 |
12 |
13 | @SuppressLint("MissingPermission") //Permissions will be set by the app, not the library
14 | fun WifiManager.addOrLookupNetwork(
15 | config: WifiConfiguration,
16 | logger: MNetLogger,
17 | ): Int {
18 | val existingNetwork = configuredNetworks.firstOrNull {
19 | it.SSID == config.SSID && it.status != WifiConfiguration.Status.DISABLED
20 | }
21 |
22 | logger(Log.DEBUG, "addOrLookupNetwork: existingNetworkId=${existingNetwork?.networkId}", null)
23 | return existingNetwork?.networkId ?: addNetwork(config)
24 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
20 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/WifiP2pGroupExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import android.net.wifi.p2p.WifiP2pGroup
4 | import android.os.Build
5 | import com.ustadmobile.meshrabiya.vnet.wifi.ConnectBand
6 |
7 | fun WifiP2pGroup.toPrettyString(): String {
8 | return buildString {
9 | val frequencyStr = if(Build.VERSION.SDK_INT >= 29) " frequency=$frequency " else ""
10 | append("WifiP2pGroup: interface=${`interface`} groupOwner = $isGroupOwner, " +
11 | "networkName=$networkName, passphrase=${passphrase} frequency=$frequencyStr")
12 | }
13 | }
14 |
15 | val WifiP2pGroup.connectBand: ConnectBand
16 | get() {
17 | return when {
18 | Build.VERSION.SDK_INT < 29 -> ConnectBand.BAND_UNKNOWN
19 | frequency in 2400..2500 -> ConnectBand.BAND_2GHZ
20 | frequency in 5150 .. 5900 -> ConnectBand.BAND_5GHZ
21 | else -> ConnectBand.BAND_UNKNOWN
22 | }
23 | }
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/util/InetAddressSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.util
2 |
3 | import kotlinx.serialization.KSerializer
4 | import kotlinx.serialization.descriptors.PrimitiveKind
5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
6 | import kotlinx.serialization.descriptors.SerialDescriptor
7 | import kotlinx.serialization.encoding.Decoder
8 | import kotlinx.serialization.encoding.Encoder
9 | import java.net.InetAddress
10 |
11 | object InetAddressSerializer: KSerializer {
12 |
13 | override val descriptor: SerialDescriptor
14 | get() = PrimitiveSerialDescriptor("inetaddr", PrimitiveKind.STRING)
15 |
16 | override fun deserialize(decoder: Decoder): InetAddress {
17 | return InetAddress.getByName(decoder.decodeString())
18 | }
19 |
20 | override fun serialize(encoder: Encoder, value: InetAddress) {
21 | encoder.encodeString(value.hostAddress ?: throw IllegalArgumentException("no host addr"))
22 | }
23 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/OutputStreamExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import com.ustadmobile.meshrabiya.vnet.VirtualPacket
4 | import com.ustadmobile.meshrabiya.vnet.socket.ChainSocketInitRequest
5 | import com.ustadmobile.meshrabiya.vnet.socket.ChainSocketInitResponse
6 | import java.io.OutputStream
7 | import java.nio.ByteBuffer
8 |
9 | fun OutputStream.writeAddress(address: Int){
10 | val buffer = ByteBuffer.wrap(ByteArray(4))
11 | buffer.putInt(address)
12 | write(buffer.array())
13 | }
14 |
15 | /**
16 | * Write the given virtual packet to the receiver output stream
17 | */
18 | fun OutputStream.writeVirtualPacket(packet: VirtualPacket) {
19 | write(packet.data, packet.dataOffset, packet.datagramPacketSize)
20 | }
21 |
22 | fun OutputStream.writeChainSocketInitRequest(request: ChainSocketInitRequest) {
23 | write(request.toBytes())
24 | }
25 |
26 | fun OutputStream.writeChainSocketInitResponse(response: ChainSocketInitResponse) {
27 | write(response.toBytes())
28 | }
29 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/SoftApConfigurationExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import android.net.wifi.SoftApConfiguration
4 | import android.os.Build
5 | import androidx.annotation.RequiresApi
6 |
7 |
8 | val SoftApConfiguration.ssidCompat: String ?
9 | @RequiresApi(30)
10 | get() {
11 | return if(Build.VERSION.SDK_INT >= 33) {
12 | //As per https://developer.android.com/reference/android/net/wifi/WifiSsid#toString()
13 | // Any WiFi ssid that is in UTF-8 will be as a string with quotes.
14 | // No support for ssid with non UTF-8 SSID.
15 | wifiSsid.toString().removeSurrounding("\"")
16 | }else {
17 | @Suppress("DEPRECATION") //Required to support pre-SDK33
18 | ssid
19 | }
20 | }
21 |
22 | @RequiresApi(30)
23 | fun SoftApConfiguration.prettyPrint() : String{
24 | return "SoftApConfiguration(ssid=$ssidCompat passphrase=$passphrase bssid=$bssid " +
25 | "hidden=$isHiddenSsid securityType=$securityType)"
26 | }
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/test/VirtualNodeExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.test
2 |
3 | import com.ustadmobile.meshrabiya.vnet.VirtualNode
4 | import kotlinx.coroutines.flow.filter
5 | import kotlinx.coroutines.flow.first
6 | import kotlinx.coroutines.runBlocking
7 | import kotlinx.coroutines.withTimeout
8 | import java.net.InetAddress
9 |
10 | fun VirtualNode.connectTo(other: VirtualNode, timeout: Long = 5000) {
11 | addNewNeighborConnection(
12 | address = InetAddress.getLoopbackAddress(),
13 | port = other.localDatagramPort,
14 | neighborNodeVirtualAddr = other.addressAsInt,
15 | socket = this.datagramSocket
16 | )
17 |
18 | //wait for connections to be ready
19 | runBlocking {
20 | withTimeout(timeout) {
21 | state.filter { it.originatorMessages.containsKey(other.addressAsInt) }
22 | .first()
23 |
24 | other.state.filter {
25 | it.originatorMessages.containsKey(addressAsInt)
26 | }.first()
27 |
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/test/FileEchoSocketServer.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.test
2 |
3 | import java.io.File
4 | import java.io.FileInputStream
5 | import java.net.ServerSocket
6 | import java.util.concurrent.ExecutorService
7 | import java.util.concurrent.Executors
8 |
9 | class FileEchoSocketServer(
10 | private val file: File,
11 | port: Int = 0,
12 | executorService: ExecutorService = Executors.newSingleThreadExecutor(),
13 | ) : Runnable {
14 |
15 | private val serverSocket = ServerSocket(port)
16 |
17 | private val future = executorService.submit(this)
18 |
19 | val localPort: Int
20 | get() = serverSocket.localPort
21 |
22 |
23 | override fun run() {
24 | while(!Thread.interrupted()) {
25 | val client = serverSocket.accept()
26 | FileInputStream(file).use { fileIn ->
27 | fileIn.copyTo(client.getOutputStream())
28 | }
29 | client.close()
30 | }
31 | }
32 |
33 | fun close() {
34 | future.cancel(true)
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/test/TestVirtualNode.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.test
2 |
3 | import com.ustadmobile.meshrabiya.ext.asInetAddress
4 | import com.ustadmobile.meshrabiya.log.MNetLogger
5 | import com.ustadmobile.meshrabiya.log.MNetLoggerStdout
6 | import com.ustadmobile.meshrabiya.vnet.NodeConfig
7 | import com.ustadmobile.meshrabiya.vnet.VirtualNode
8 | import com.ustadmobile.meshrabiya.vnet.randomApipaAddr
9 | import com.ustadmobile.meshrabiya.vnet.wifi.MeshrabiyaWifiManager
10 | import kotlinx.serialization.json.Json
11 | import org.mockito.kotlin.mock
12 | import java.util.UUID
13 |
14 |
15 | class TestVirtualNode(
16 | localNodeAddress: Int = randomApipaAddr(),
17 | port: Int = 0,
18 | logger: MNetLogger = MNetLoggerStdout(),
19 | override val meshrabiyaWifiManager: MeshrabiyaWifiManager = mock { },
20 | json: Json,
21 | config: NodeConfig = NodeConfig(maxHops = 5),
22 | ) : VirtualNode(
23 | port = port,
24 | logger = logger,
25 | json = json,
26 | config = config,
27 | address = localNodeAddress.asInetAddress(),
28 | )
29 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/mmcp/MmcpHeader.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 |
6 |
7 | data class MmcpHeader(
8 | val what: Byte,
9 | val messageId: Int,
10 | ) {
11 |
12 | fun toBytes(
13 | byteArray: ByteArray,
14 | offset: Int
15 | ) {
16 | val headerBuf = ByteBuffer
17 | .wrap(byteArray, offset, MmcpMessage.MMCP_HEADER_LEN)
18 | .order(ByteOrder.BIG_ENDIAN)
19 | headerBuf.put(what)
20 | headerBuf.putInt(messageId)
21 | }
22 |
23 |
24 |
25 | companion object {
26 |
27 | fun fromBytes(
28 | byteArray: ByteArray,
29 | offset: Int
30 | ): MmcpHeader {
31 | val headerBuf = ByteBuffer
32 | .wrap(byteArray, offset, MmcpMessage.MMCP_HEADER_LEN)
33 | .order(ByteOrder.BIG_ENDIAN)
34 | val what = headerBuf.get()
35 | val messageId = headerBuf.int
36 |
37 | return MmcpHeader(what, messageId)
38 | }
39 |
40 | }
41 | }
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/test/EchoDatagramServer.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.test
2 |
3 | import java.net.DatagramPacket
4 | import java.net.DatagramSocket
5 | import java.util.concurrent.ExecutorService
6 | import java.util.concurrent.Future
7 |
8 | class EchoDatagramServer(
9 | port: Int,
10 | executor: ExecutorService,
11 | ) : Runnable {
12 |
13 | val datagramSocket = DatagramSocket(port)
14 |
15 | val future: Future<*>
16 |
17 | val listeningPort = datagramSocket.localPort
18 |
19 | init {
20 | future = executor.submit(this)
21 | }
22 |
23 | override fun run() {
24 | val buf = ByteArray(1500)
25 | val packet = DatagramPacket(buf, buf.size)
26 | while(!Thread.interrupted()) {
27 | datagramSocket.receive(packet)
28 |
29 | val replyPacket = DatagramPacket(buf, 0, packet.length, packet.address, packet.port)
30 | datagramSocket.send(replyPacket)
31 | }
32 | }
33 |
34 | fun close() {
35 | future.cancel(true)
36 | datagramSocket.close()
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/server/InputStreamCounter.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.server
2 |
3 | import java.io.FilterInputStream
4 | import java.io.InputStream
5 |
6 | class InputStreamCounter(
7 | `in`: InputStream
8 | ): FilterInputStream(`in`){
9 |
10 | @Volatile
11 | var bytesRead: Int = 0
12 | private set
13 |
14 | @Volatile
15 | var closed: Boolean = false
16 | private set
17 |
18 | override fun read(): Int {
19 | return super.read().also {
20 | if(it != -1)
21 | bytesRead++
22 | }
23 | }
24 |
25 | override fun read(b: ByteArray): Int {
26 | return super.read(b).also {
27 | if(it != -1)
28 | bytesRead += it
29 | }
30 | }
31 |
32 | override fun read(b: ByteArray, off: Int, len: Int): Int {
33 | return super.read(b, off, len).also {
34 | if(it != -1)
35 | bytesRead += it
36 | }
37 | }
38 |
39 | override fun close() {
40 | super.close()
41 | closed = true
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/test/VirtualPacketTestUtil.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.test
2 |
3 | import com.ustadmobile.meshrabiya.vnet.VirtualPacket
4 | import com.ustadmobile.meshrabiya.vnet.VirtualPacketHeader
5 | import kotlin.random.Random
6 |
7 | fun newVirtualPacketWithRandomPayload(
8 | toAddr: Int,
9 | toPort: Int,
10 | fromAddr: Int,
11 | fromPort: Int,
12 | lastHopAddr: Int = fromAddr,
13 | payloadSize: Int
14 | ): VirtualPacket {
15 | val buffer = ByteArray(payloadSize + VirtualPacket.VIRTUAL_PACKET_BUF_SIZE)
16 | Random.Default.nextBytes(buffer, VirtualPacketHeader.HEADER_SIZE)
17 | return VirtualPacket.fromHeaderAndPayloadData(
18 | header = VirtualPacketHeader(
19 | toAddr = toAddr,
20 | toPort = toPort,
21 | fromAddr = fromAddr,
22 | fromPort = fromPort,
23 | lastHopAddr = lastHopAddr,
24 | hopCount = 0,
25 | maxHops = 8,
26 | payloadSize = payloadSize
27 | ),
28 | data = buffer,
29 | payloadOffset = VirtualPacketHeader.HEADER_SIZE
30 | )
31 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/WifiP2pActionListenerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import android.net.wifi.p2p.WifiP2pManager
4 | import android.util.Log
5 | import com.ustadmobile.meshrabiya.log.MNetLogger
6 | import kotlinx.coroutines.CompletableDeferred
7 |
8 | class WifiP2pActionListenerAdapter(
9 | val onFailLogMessage: String,
10 | val logger: MNetLogger? = null,
11 | val onSuccessLogMessage: String? = null,
12 | ): WifiP2pManager.ActionListener {
13 |
14 | private val completable = CompletableDeferred()
15 |
16 | override fun onSuccess() {
17 | if(onSuccessLogMessage != null) {
18 | logger?.invoke(Log.DEBUG, onSuccessLogMessage, null)
19 | }
20 | completable.complete(true)
21 | }
22 |
23 | override fun onFailure(reason: Int) {
24 | logger?.invoke(Log.WARN, "WifiP2pActionListener: $onFailLogMessage : " +
25 | "reason=${WifiDirectError.errorString(reason)}")
26 | completable.completeExceptionally(WifiDirectException(onFailLogMessage, reason))
27 | }
28 |
29 | suspend fun await() = completable.await()
30 | }
31 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/socket/ChainSocketExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.socket
2 |
3 | import com.ustadmobile.meshrabiya.ext.readChainInitResponse
4 | import com.ustadmobile.meshrabiya.ext.writeChainSocketInitRequest
5 | import java.io.IOException
6 | import java.net.Socket
7 |
8 | /**
9 | * Where this socket is not connected to the intended final destination, then we need to write the
10 | * ChainInitRequest to the output
11 | */
12 | fun Socket.initializeChainIfNotFinalDest(
13 | chainInitRequest: ChainSocketInitRequest,
14 | nextHop: ChainSocketNextHop,
15 | ) {
16 | if(!nextHop.isFinalDest) {
17 | println("${nextHop.address}:${nextHop.port} is not final destination - write init request and get response")
18 | getOutputStream().writeChainSocketInitRequest(chainInitRequest)
19 | val initResponse = getInputStream().readChainInitResponse()
20 | println("${nextHop.address}:${nextHop.port} got init response")
21 |
22 | if(initResponse.statusCode != 200){
23 | throw IOException("Could not init chain socket: status code = ${initResponse.statusCode}")
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/test-shared/src/main/java/com/ustadmobile/meshrabiya/FileExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya
2 |
3 | import java.io.File
4 | import java.io.FileInputStream
5 | import java.security.DigestInputStream
6 | import java.security.MessageDigest
7 | import kotlin.random.Random
8 |
9 |
10 | fun File.writeRandomData(size: Int) {
11 | val buf = ByteArray(8192)
12 | var bytesWritten = 0
13 | outputStream().use { outStream ->
14 | while(bytesWritten < size) {
15 | val len = minOf(buf.size, size - bytesWritten)
16 | Random.nextBytes(buf, 0, len)
17 | outStream.write(buf, 0, len)
18 | bytesWritten += len
19 | }
20 | outStream.flush()
21 | }
22 | }
23 |
24 | fun File.md5sum(): ByteArray {
25 | val messageDigest = MessageDigest.getInstance("MD5")
26 | val inStream = FileInputStream(this)
27 | val digestInputStream = DigestInputStream(inStream, messageDigest)
28 |
29 | val buf = ByteArray(8192)
30 | digestInputStream.use {
31 | while(inStream.read(buf) != -1) {
32 | //do nothing - just read through to get the md5sum
33 | }
34 | }
35 |
36 | return messageDigest.digest()
37 | }
38 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/state/LocalOnlyHotspotState.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi.state
2 |
3 | import android.net.wifi.WifiManager.LocalOnlyHotspotCallback
4 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotStatus
5 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
6 |
7 | data class LocalOnlyHotspotState(
8 | val status: HotspotStatus = HotspotStatus.STOPPED,
9 | val config: WifiConnectConfig? = null,
10 | val error: Int = 0,
11 | ) {
12 |
13 | companion object {
14 |
15 | //As per https://developer.android.com/reference/android/net/wifi/WifiManager.LocalOnlyHotspotCallback#onFailed(int)
16 | fun errorCodeToString(errorCode: Int) : String{
17 | return when(errorCode) {
18 | LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED -> "ERROR_TETHERING_DISALLOWED"
19 | LocalOnlyHotspotCallback.ERROR_NO_CHANNEL -> "ERROR_NO_CHANNEL"
20 | LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE -> "ERROR_INCOMPATIBLE_MODE"
21 | LocalOnlyHotspotCallback.ERROR_GENERIC -> "ERROR_GENERIC"
22 | else -> "Unknown ERROR: $errorCode"
23 | }
24 | }
25 |
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/log/MNetLoggerStdout.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.log
2 |
3 | import android.util.Log
4 | import java.util.concurrent.atomic.AtomicInteger
5 |
6 | class MNetLoggerStdout(
7 | private val minLogLevel: Int = Log.VERBOSE,
8 | ): MNetLogger() {
9 |
10 | private val lineIdAtomic = AtomicInteger()
11 |
12 | private val epochTime = System.currentTimeMillis()
13 |
14 | private fun doLog(priority: Int, message: String, exception: Exception?) {
15 | val line = LogLine(message, priority, System.currentTimeMillis(), lineIdAtomic.incrementAndGet())
16 | println(buildString {
17 | append(line.toString(epochTime))
18 | if(exception != null) {
19 | append(" ")
20 | append(exception.stackTraceToString())
21 | }
22 | })
23 | }
24 |
25 | override fun invoke(priority: Int, message: String, exception: Exception?) {
26 | if(priority >= minLogLevel)
27 | doLog(priority, message, exception)
28 | }
29 |
30 | override fun invoke(priority: Int, message: () -> String, exception: Exception?) {
31 | if(priority >= minLogLevel)
32 | doLog(priority, message(), exception)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/state/WifiStationState.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi.state
2 |
3 | import android.net.Network
4 | import com.ustadmobile.meshrabiya.vnet.VirtualNodeDatagramSocket
5 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
6 |
7 | /**
8 | * The Wifi station state - e.g. the 'client' connection.
9 | *
10 | * @param status the current status of station mode as being used by the Meshrabiya node
11 | * @param network the network object for the currently connected station network (if any)
12 | * @param config the config that we are connected or connecting to for station mode (if any)
13 | * @param stationBoundSocketsPort the port number for station bound sockets - see MeshrabiyaWifiManagerAndroid.stationBoundSockets
14 | */
15 | data class WifiStationState(
16 | val status: Status = Status.INACTIVE,
17 | val network: Network? = null,
18 | val config: WifiConnectConfig? = null,
19 | val stationBoundSocketsPort: Int = -1,
20 | val stationBoundDatagramSocket: VirtualNodeDatagramSocket? = null,
21 | ) {
22 |
23 | enum class Status {
24 | INACTIVE, CONNECTING, AVAILABLE, UNAVAILABLE, LOST;
25 |
26 | companion object {
27 |
28 | val FAIL_STATES = listOf(UNAVAILABLE, LOST)
29 | }
30 | }
31 |
32 |
33 |
34 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/wifi/HotspotResponseTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import com.ustadmobile.meshrabiya.ext.requireAsIpv6
4 | import com.ustadmobile.meshrabiya.vnet.randomApipaAddr
5 | import org.junit.Assert
6 | import org.junit.Test
7 | import java.net.Inet6Address
8 | import kotlin.random.Random
9 |
10 | class HotspotResponseTest {
11 |
12 | @Test
13 | fun givenHotspotResponse_whenConvertedToAndFromBytes_thenShouldBeEqual() {
14 | val response = LocalHotspotResponse(
15 | responseToMessageId = Random.nextInt(),
16 | errorCode = 0,
17 | config = WifiConnectConfig(
18 | nodeVirtualAddr = randomApipaAddr(),
19 | ssid = "test",
20 | passphrase = "world",
21 | port = 8042,
22 | hotspotType = HotspotType.LOCALONLY_HOTSPOT,
23 | linkLocalAddr = Inet6Address.getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334").requireAsIpv6(),
24 | ),
25 | redirectAddr = 0,
26 | )
27 |
28 | val responseArr = ByteArray(response.sizeInBytes + 10)
29 | response.toBytes(responseArr, 10)
30 |
31 | val responseFromBytes = LocalHotspotResponse.fromBytes(responseArr, 10)
32 | Assert.assertEquals(response, responseFromBytes)
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/ext/ContentResolverExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.ext
2 |
3 | import android.content.ContentResolver
4 | import android.net.Uri
5 | import android.provider.OpenableColumns
6 | import androidx.core.net.toFile
7 |
8 | data class UriNameAndSize(
9 | val name: String?,
10 | val size: Long,
11 | )
12 |
13 | fun ContentResolver.getUriNameAndSize(uri: Uri): UriNameAndSize {
14 | return if(uri.scheme == "file") {
15 | val uriFile = uri.toFile()
16 | UriNameAndSize(uriFile.name, uriFile.length())
17 | }else {
18 | query(
19 | uri, null, null, null, null
20 | )?.use { cursor ->
21 | var nameIndex = 0
22 | var sizeIndex = 0
23 | if(cursor.moveToFirst() &&
24 | cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).also { nameIndex = it } >= 1 &&
25 | cursor.getColumnIndex(OpenableColumns.SIZE).also { sizeIndex = it } >= 1
26 | ) {
27 | val size = if(cursor.isNull(sizeIndex)) { null } else {
28 | cursor.getString(sizeIndex)
29 | }
30 | UriNameAndSize(cursor.getString(nameIndex), size?.toLong() ?: -1L)
31 | }else {
32 | null
33 | }
34 | } ?: UriNameAndSize(null, -1)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/MeshrabiyaConnectLinkTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import com.ustadmobile.meshrabiya.ext.requireAsIpv6
4 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
5 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotType
6 | import kotlinx.serialization.json.Json
7 | import org.junit.Assert
8 | import org.junit.Test
9 | import java.net.Inet6Address
10 |
11 | class MeshrabiyaConnectLinkTest {
12 |
13 | @Test
14 | fun givenLinkFromComponents_whenParsed_thenShouldMatchOriginal(){
15 | val json = Json { encodeDefaults = true }
16 | val link = MeshrabiyaConnectLink.fromComponents(
17 | nodeAddr = randomApipaAddr(),
18 | port = 8087,
19 | hotspotConfig = WifiConnectConfig(
20 | nodeVirtualAddr = randomApipaAddr(),
21 | ssid = "test",
22 | passphrase = "testpass",
23 | port = 8087,
24 | hotspotType = HotspotType.LOCALONLY_HOTSPOT,
25 | linkLocalAddr = Inet6Address.getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334").requireAsIpv6(),
26 | ),
27 | bluetoothConfig = null,
28 | json = json,
29 | )
30 |
31 | val parsedLink = MeshrabiyaConnectLink.parseUri(link.uri, json)
32 | Assert.assertEquals(link, parsedLink)
33 |
34 |
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/util/UuidMaskUtil.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.util
2 |
3 | import java.util.UUID
4 |
5 |
6 | /**
7 | * Using a UUID mask all clients share the same UUID mask (the first 112 bits of the UUID) and the
8 | * remaining 16 bits are randomly allocated. This allows a preset to be shared without any two clients
9 | * using exactly the same UUID.
10 | *
11 | * This also allows each node to have a single port that can then be used to determine the final UUID
12 | * e.g. if the mask and port are known, we know the UUID that will be used for a particular service.
13 | */
14 | fun uuidForMaskAndPort(mask: UUID, port: Int): UUID{
15 | val newLeastSigBits = mask.leastSignificantBits.shl(16)
16 | .or(port.toLong())
17 | return UUID(mask.mostSignificantBits, newLeastSigBits)
18 | }
19 |
20 | /**
21 | * Return the port portion of this UUID if it was created using uuidForMaskAndPort
22 | */
23 | fun UUID.maskedPort(): Int {
24 | return leastSignificantBits.and(0xffff).toInt()
25 | }
26 |
27 | /**
28 | * Check if given UUID matches a mask (e.g. as used in uuidForMaskAndPort)
29 | */
30 | fun UUID.matchesMask(mask: UUID): Boolean {
31 | if(mask.mostSignificantBits != mostSignificantBits)
32 | return false
33 |
34 | //Cut off the rightmost 16 bits (e.g. where the port is stored)
35 | val uuidLeastSigBitsWithoutPort = leastSignificantBits.shr(16).shl(16)
36 | return mask.leastSignificantBits.shl(16) == uuidLeastSigBitsWithoutPort
37 | }
38 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/VirtualNodeReturnPathSocketFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import com.ustadmobile.meshrabiya.ext.findLocalInetAddressForDestinationAddress
4 | import com.ustadmobile.meshrabiya.ext.prefixMatches
5 | import com.ustadmobile.meshrabiya.portforward.ReturnPathSocketFactory
6 | import java.lang.IllegalArgumentException
7 | import java.net.DatagramSocket
8 | import java.net.InetAddress
9 |
10 | /**
11 | * Implementation of return path socket factory that can create an IDatagramSocket for the real
12 | * network or a virtual datagram socket.
13 | *
14 | * If a destination is on the real network, then the created socket will be bound to the network
15 | * interface where the netmask matches the given destination address.
16 | */
17 | class VirtualNodeReturnPathSocketFactory(
18 | private val node: VirtualNode,
19 | ): ReturnPathSocketFactory {
20 |
21 |
22 | override fun createSocket(destAddress: InetAddress, port: Int): DatagramSocket {
23 | return if(
24 | destAddress.address.prefixMatches(node.networkPrefixLength, node.address.address)
25 | ) {
26 | node.createBoundDatagramSocket(0)
27 | }else{
28 | val bindAddress = findLocalInetAddressForDestinationAddress(destAddress)
29 |
30 | return bindAddress?.let { DatagramSocket(0, it) }
31 | ?: throw IllegalArgumentException("Could not find network interface with subnet " +
32 | "mask for dest address $destAddress")
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/ConnectBand.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import kotlinx.serialization.KSerializer
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.descriptors.PrimitiveKind
6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 |
11 | @Serializable(with = ConnectBandSerializer::class)
12 | enum class ConnectBand(val flag: Byte) {
13 | //Ids are as per WifiP2pConfig GROUP_OWNER_BAND_
14 | BAND_2GHZ(1), BAND_5GHZ(2), BAND_UNKNOWN(0),
15 | ;
16 |
17 | override fun toString(): String {
18 | return when(this) {
19 | BAND_2GHZ -> "2Ghz"
20 | BAND_5GHZ -> "5Ghz"
21 | BAND_UNKNOWN -> "Band unknown"
22 | }
23 | }
24 |
25 | companion object {
26 | fun fromFlag(flag: Byte): ConnectBand {
27 | return values().first { it.flag == flag }
28 | }
29 |
30 | }
31 | }
32 |
33 | object ConnectBandSerializer: KSerializer {
34 | override fun deserialize(decoder: Decoder): ConnectBand {
35 | return ConnectBand.fromFlag(decoder.decodeByte())
36 | }
37 |
38 | override val descriptor: SerialDescriptor
39 | get() = PrimitiveSerialDescriptor("connectBand", PrimitiveKind.BYTE)
40 |
41 | override fun serialize(encoder: Encoder, value: ConnectBand) {
42 | encoder.encodeByte(value.flag)
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/mmcp/MmcpPong.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 |
6 | /**
7 | * @param replyToMessageId the message ID of the ping that this is a reply to
8 | */
9 | class MmcpPong(
10 | messageId: Int,
11 | val replyToMessageId: Int,
12 | ): MmcpMessage(WHAT_PONG, messageId) {
13 | override fun toBytes() = headerAndPayloadToBytes(header,
14 | ByteBuffer.wrap(ByteArray(4))
15 | .order(ByteOrder.BIG_ENDIAN)
16 | .putInt(replyToMessageId)
17 | .array())
18 |
19 | override fun equals(other: Any?): Boolean {
20 | if (this === other) return true
21 | if (other !is MmcpPong) return false
22 | if (!super.equals(other)) return false
23 |
24 | if (replyToMessageId != other.replyToMessageId) return false
25 |
26 | return true
27 | }
28 |
29 | override fun hashCode(): Int {
30 | var result = super.hashCode()
31 | result = 31 * result + replyToMessageId
32 | return result
33 | }
34 |
35 |
36 | companion object {
37 |
38 | fun fromBytes(
39 | byteArray: ByteArray,
40 | offset: Int = 0,
41 | len: Int = byteArray.size,
42 | ): MmcpPong {
43 | val (header, payload) = mmcpHeaderAndPayloadFromBytes(byteArray, offset, len)
44 | val replyToMessageId = ByteBuffer.wrap(payload)
45 | .order(ByteOrder.BIG_ENDIAN)
46 | .int
47 |
48 | return MmcpPong(header.messageId, replyToMessageId)
49 | }
50 |
51 | }
52 | }
--------------------------------------------------------------------------------
/test-shared/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'org.jetbrains.kotlin.plugin.serialization'
5 | }
6 |
7 | android {
8 | namespace 'com.ustadmobile.meshrabiya.test'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | minSdk 26
13 | targetSdk 33
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 |
26 | compileOptions {
27 | coreLibraryDesugaringEnabled true
28 |
29 | sourceCompatibility JavaVersion.VERSION_17
30 | targetCompatibility JavaVersion.VERSION_17
31 | }
32 | kotlinOptions {
33 | jvmTarget = JavaVersion.VERSION_17
34 | }
35 | }
36 |
37 | dependencies {
38 | implementation project(':lib-meshrabiya')
39 | implementation 'androidx.core:core-ktx:1.8.0'
40 | implementation 'junit:junit:4.13.2'
41 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version_coroutines"
42 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$version_kotlinx_serialization"
43 | implementation "org.mockito.kotlin:mockito-kotlin:$version_kotlin_mockito"
44 | implementation "app.cash.turbine:turbine:$version_turbine"
45 | implementation "com.squareup.okhttp3:mockwebserver:$version_mockwebserver"
46 | implementation "com.squareup.okhttp3:okhttp:$version_okhttp"
47 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/mmcp/MmcpHotspotResponse.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import com.ustadmobile.meshrabiya.vnet.wifi.LocalHotspotResponse
4 |
5 | class MmcpHotspotResponse(
6 | messageId: Int,
7 | val result: LocalHotspotResponse,
8 | ) : MmcpMessage(WHAT_HOTSPOT_RESPONSE, messageId) {
9 |
10 | override fun toBytes(): ByteArray {
11 | return headerAndPayloadToBytes(header, result.toBytes())
12 | }
13 |
14 | override fun equals(other: Any?): Boolean {
15 | if (this === other) return true
16 | if (other !is MmcpHotspotResponse) return false
17 | if (!super.equals(other)) return false
18 |
19 | if (result != other.result) return false
20 |
21 | return true
22 | }
23 |
24 | override fun hashCode(): Int {
25 | var result1 = super.hashCode()
26 | result1 = 31 * result1 + result.hashCode()
27 | return result1
28 | }
29 |
30 |
31 | companion object {
32 |
33 | fun fromBytes(
34 | byteArray: ByteArray,
35 | offset: Int = 0,
36 | len: Int = byteArray.size,
37 | ): MmcpHotspotResponse {
38 | try {
39 | val (header, payload) = mmcpHeaderAndPayloadFromBytes(
40 | byteArray, offset, len
41 | )
42 |
43 | val response = LocalHotspotResponse.fromBytes(payload, 0)
44 | return MmcpHotspotResponse(header.messageId, response)
45 | }catch(e: Exception) {
46 | println("FFS")
47 | e.printStackTrace()
48 | throw e
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/ByteArrayExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 | import kotlin.experimental.and
6 | import kotlin.math.min as mathMin
7 |
8 | fun ByteArray.ip4AddressToInt() : Int{
9 | return ByteBuffer.wrap(this).order(ByteOrder.BIG_ENDIAN).int
10 | }
11 |
12 | /**
13 | * Check if two network addresses match up to the given network prefix length.
14 | *
15 | * @receiver a ByteArray that represents a network address
16 | * @param networkPrefixLength the netmask prefix length to check (in bits). e.g. to check a /16 match then 16, etc.
17 | * @param otherAddress a ByteArray that represents another network address
18 | *
19 | * @return true if the addresses are the same for the first networkPrefixLength bits, false otherwise
20 | */
21 | fun ByteArray.prefixMatches(
22 | networkPrefixLength: Int,
23 | otherAddress: ByteArray
24 | ) : Boolean {
25 | var bitsCompared = 0
26 | var bitsToCompare = 0
27 |
28 | var index = 0
29 | var mask : Byte
30 | while(bitsCompared < networkPrefixLength) {
31 | bitsToCompare = mathMin(8, networkPrefixLength - bitsCompared)
32 |
33 | if(bitsToCompare == 8) {
34 | if(this[index] != otherAddress[index])
35 | return false
36 | }else {
37 | for(b in 0 until bitsToCompare) {
38 | mask = 1.shl(b).toByte()
39 |
40 | if(this[index].and(mask) != otherAddress[index].and(mask))
41 | return false
42 | }
43 | }
44 |
45 | bitsCompared += bitsToCompare
46 | index++
47 | }
48 |
49 | return true
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/VirtualRouter.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import com.ustadmobile.meshrabiya.vnet.datagram.VirtualDatagramSocketImpl
4 | import com.ustadmobile.meshrabiya.vnet.socket.ChainSocketNextHop
5 | import java.net.DatagramPacket
6 | import java.net.InetAddress
7 |
8 | /**
9 | * Represents the netwrok
10 | */
11 | interface VirtualRouter {
12 |
13 | val address: InetAddress
14 |
15 | val networkPrefixLength: Int
16 |
17 | /**
18 | * Route the given incoming packet.
19 | *
20 | * @param packet the packet received
21 | */
22 | fun route(
23 | packet: VirtualPacket,
24 | datagramPacket: DatagramPacket? = null,
25 | virtualNodeDatagramSocket: VirtualNodeDatagramSocket? = null,
26 | )
27 |
28 | /**
29 | * When using chain sockets this function will lookup the next hop for the given virtual
30 | * address.
31 | */
32 | fun lookupNextHopForChainSocket(
33 | address: InetAddress,
34 | port: Int,
35 | ): ChainSocketNextHop
36 |
37 | fun nextMmcpMessageId(): Int
38 |
39 | /**
40 | * The default datagram socket local port (not bound to any network). Used to send/receive
41 | * VirtualPackets over the real network.
42 | */
43 | val localDatagramPort: Int
44 |
45 |
46 | /**
47 | * Allocate a port on the virtual router
48 | */
49 | fun allocateUdpPortOrThrow(
50 | virtualDatagramSocketImpl: VirtualDatagramSocketImpl,
51 | portNum: Int
52 | ): Int
53 |
54 | fun deallocatePort(
55 | protocol: Protocol,
56 | portNum: Int
57 | )
58 |
59 |
60 | companion object {
61 |
62 |
63 |
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/mmcp/MmcpHotspotResponseTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import com.ustadmobile.meshrabiya.ext.requireAsIpv6
4 | import com.ustadmobile.meshrabiya.vnet.randomApipaAddr
5 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
6 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotType
7 | import com.ustadmobile.meshrabiya.vnet.wifi.LocalHotspotResponse
8 | import org.junit.Assert
9 | import org.junit.Test
10 | import java.net.Inet6Address
11 | import kotlin.random.Random
12 |
13 | class MmcpHotspotResponseTest {
14 |
15 | @Test
16 | fun givenHotspotResponse_whenConvertedToFromBytes_thenShouldBeEqual() {
17 | val responseMessage = MmcpHotspotResponse(
18 | messageId = 42,
19 | result = LocalHotspotResponse(
20 | responseToMessageId = Random.nextInt(),
21 | errorCode = 0,
22 | config = WifiConnectConfig(
23 | nodeVirtualAddr = randomApipaAddr(),
24 | ssid = "test",
25 | passphrase = "secret",
26 | port = 8042,
27 | hotspotType = HotspotType.LOCALONLY_HOTSPOT,
28 | linkLocalAddr = Inet6Address.getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334").requireAsIpv6(),
29 | ),
30 | redirectAddr = 0
31 | )
32 | )
33 |
34 | val responseBytes = responseMessage.toBytes()
35 | val responseDeserialized = MmcpHotspotResponse.fromBytes(responseBytes) as MmcpHotspotResponse
36 | Assert.assertEquals(responseMessage.messageId, responseDeserialized.messageId)
37 | Assert.assertEquals(responseMessage.result, responseDeserialized.result)
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/mmcp/MmcpAck.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 |
6 | /**
7 | * An MMCP message may request an acknowledgement (ACK) reply - e.g. to be sure that the original
8 | * message was received.
9 | */
10 | class MmcpAck(
11 | messageId: Int,
12 | val ackOfMessageId: Int,
13 | ) : MmcpMessage(
14 | what = WHAT_ACK,
15 | messageId = messageId,
16 | ){
17 |
18 | override fun toBytes() = headerAndPayloadToBytes(header,
19 | ByteBuffer.wrap(ByteArray(MESSAGE_SIZE))
20 | .order(ByteOrder.BIG_ENDIAN)
21 | .putInt(ackOfMessageId)
22 | .array())
23 |
24 | override fun equals(other: Any?): Boolean {
25 | if (this === other) return true
26 | if (other !is MmcpAck) return false
27 | if (!super.equals(other)) return false
28 |
29 | if (ackOfMessageId != other.ackOfMessageId) return false
30 |
31 | return true
32 | }
33 |
34 | override fun hashCode(): Int {
35 | var result = super.hashCode()
36 | result = 31 * result + ackOfMessageId
37 | return result
38 | }
39 |
40 | companion object {
41 |
42 | //Size = size of ackOfMessageId = 4 bytes
43 | const val MESSAGE_SIZE = 4
44 |
45 | fun fromBytes(
46 | byteArray: ByteArray,
47 | offset: Int = 0,
48 | len: Int = byteArray.size,
49 | ): MmcpAck {
50 | val (header, payload) = mmcpHeaderAndPayloadFromBytes(byteArray, offset, len)
51 | val ackOfMessageId = ByteBuffer.wrap(payload).int
52 | return MmcpAck(messageId = header.messageId, ackOfMessageId = ackOfMessageId)
53 | }
54 |
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/VirtualPacketStreamTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import com.ustadmobile.meshrabiya.ext.readVirtualPacket
4 | import com.ustadmobile.meshrabiya.ext.writeVirtualPacket
5 | import org.junit.Assert
6 | import org.junit.Test
7 | import java.io.ByteArrayInputStream
8 | import java.io.ByteArrayOutputStream
9 | import kotlin.random.Random
10 |
11 | class VirtualPacketStreamTest {
12 |
13 | @Test
14 | fun givenVirtualPacketWrittenToOutputStream_whenReadFromInputStream_thenWillMatch() {
15 | val payloadSize = 1000
16 | val data = Random.nextBytes(ByteArray(1000 + VirtualPacketHeader.HEADER_SIZE))
17 | val header = VirtualPacketHeader(
18 | toAddr = 1000,
19 | toPort = 8080,
20 | fromAddr = 1002,
21 | fromPort = 8072,
22 | lastHopAddr = 1002,
23 | hopCount = 1,
24 | maxHops = 4,
25 | payloadSize = payloadSize,
26 | )
27 |
28 | val outStream = ByteArrayOutputStream()
29 | val packet = VirtualPacket.fromHeaderAndPayloadData(
30 | header = header,
31 | data = data,
32 | payloadOffset = VirtualPacketHeader.HEADER_SIZE,
33 | )
34 | outStream.writeVirtualPacket(packet)
35 | outStream.flush()
36 |
37 | val writtenBytes = outStream.toByteArray()
38 |
39 | val inStream = ByteArrayInputStream(writtenBytes)
40 |
41 | val buf = ByteArray(8000)
42 | val packetIn = inStream.readVirtualPacket(buf, 0)
43 |
44 | Assert.assertEquals("Header matches", header, packetIn?.header)
45 | data.forEachIndexed { index, byte ->
46 | Assert.assertEquals(byte, packetIn!!.data[index])
47 | }
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/test-app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/viewmodel/InfoViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.ustadmobile.meshrabiya.log.MNetLogger
6 | import com.ustadmobile.meshrabiya.log.LogLine
7 | import com.ustadmobile.meshrabiya.testapp.MNetLoggerAndroid
8 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
9 | import com.ustadmobile.meshrabiya.testapp.appstate.FabState
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.flow.update
14 | import kotlinx.coroutines.launch
15 | import org.kodein.di.DI
16 | import org.kodein.di.direct
17 | import org.kodein.di.instance
18 |
19 | data class InfoUiState(
20 | val recentLogs: List = emptyList(),
21 | val appUiState: AppUiState = AppUiState(),
22 | )
23 |
24 | class InfoViewModel(
25 | di: DI
26 | ) : ViewModel(){
27 |
28 | private val _uiState = MutableStateFlow(InfoUiState())
29 |
30 | val uiState: Flow = _uiState.asStateFlow()
31 |
32 | private val loggerAndroid: MNetLoggerAndroid = di.direct.instance() as MNetLoggerAndroid
33 |
34 | init {
35 | _uiState.update {prev ->
36 | prev.copy(
37 | appUiState = AppUiState(
38 | title = "Info",
39 | fabState = FabState(visible = false),
40 | )
41 | )
42 | }
43 |
44 | viewModelScope.launch {
45 | loggerAndroid.recentLogs.collect {
46 | _uiState.update { prev ->
47 | prev.copy(
48 | recentLogs = it
49 | )
50 | }
51 | }
52 | }
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/InetAddressExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import java.net.Inet6Address
4 | import java.net.InetAddress
5 | import java.net.NetworkInterface
6 |
7 | fun InetAddress.requireAddressAsInt(): Int {
8 | val addrData = address
9 | if(addrData.size != 4)
10 | throw IllegalArgumentException("requireAddressAsInt: not 32-bit address")
11 |
12 | return addrData.ip4AddressToInt()
13 | }
14 |
15 | fun InetAddress.requireAsIpv6() : Inet6Address {
16 | return this as? Inet6Address ?: throw IllegalStateException("$this not an ipv6 address")
17 | }
18 |
19 | fun unspecifiedIpv6Address() = Inet6Address.getByName("::").requireAsIpv6()
20 |
21 | fun InetAddress.prefixMatches(
22 | networkPrefixLength: Int,
23 | other: InetAddress
24 | ) : Boolean {
25 | return address.prefixMatches(networkPrefixLength, other.address)
26 | }
27 |
28 | /**
29 | * Find a local InetAddress (if any) where the address and network prefix matches the destination.
30 | *
31 | * @param destAddress The destination address as above
32 | *
33 | * @return InetAddress that is a local network interface where the address matches the destination
34 | * up to the network prefix length of its interface, null if there is no match
35 | */
36 | fun findLocalInetAddressForDestinationAddress(
37 | destAddress: InetAddress
38 | ) : InetAddress? {
39 | return NetworkInterface.getNetworkInterfaces().firstNotNullOfOrNull { netInterface ->
40 | netInterface.interfaceAddresses.firstNotNullOfOrNull { interfaceAddress ->
41 | if(interfaceAddress.address.prefixMatches(
42 | interfaceAddress.networkPrefixLength.toInt(), destAddress)
43 | ) {
44 | interfaceAddress.address
45 | }else {
46 | null
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/socket/ChainSocketInitRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.socket
2 |
3 | import com.ustadmobile.meshrabiya.ext.getInet4Address
4 | import com.ustadmobile.meshrabiya.ext.putInet4Address
5 | import java.net.InetAddress
6 | import java.nio.ByteBuffer
7 | import java.nio.ByteOrder
8 |
9 | /**
10 | * When running a ChainSocket, the init request will be written first.
11 | */
12 | data class ChainSocketInitRequest(
13 | val virtualDestAddr: InetAddress,
14 | val virtualDestPort: Int,
15 | val fromAddr: InetAddress,
16 | val hopCount: Byte = 0,
17 | ) {
18 |
19 | fun toBytes(): ByteArray {
20 | val byteArr = ByteArray(MESSAGE_SIZE)
21 | val byteBuffer = ByteBuffer.wrap(byteArr)
22 | .order(ByteOrder.BIG_ENDIAN)
23 | byteBuffer.putInet4Address(virtualDestAddr)
24 | byteBuffer.putInt(virtualDestPort)
25 | byteBuffer.putInet4Address(fromAddr)
26 | byteBuffer.put(hopCount)
27 |
28 | return byteArr
29 | }
30 |
31 | companion object {
32 | const val MESSAGE_SIZE = 4 + 4 + 4 + 1
33 |
34 | fun fromBytes(
35 | byteArray: ByteArray,
36 | offset: Int = 0
37 | ) : ChainSocketInitRequest {
38 | val byteBuf = ByteBuffer.wrap(byteArray, offset, MESSAGE_SIZE)
39 | .order(ByteOrder.BIG_ENDIAN)
40 | val virtualDestADdr = byteBuf.getInet4Address()
41 | val virtualDestPort = byteBuf.getInt()
42 | val fromAddr = byteBuf.getInet4Address()
43 | val hopCount = byteBuf.get()
44 |
45 | return ChainSocketInitRequest(
46 | virtualDestAddr = virtualDestADdr,
47 | virtualDestPort = virtualDestPort,
48 | fromAddr = fromAddr,
49 | hopCount = hopCount,
50 | )
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/ext/ByteBufferExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.ext
2 |
3 | import java.net.InetAddress
4 | import java.nio.ByteBuffer
5 |
6 | /**
7 | * Put a string into the byte buffer (based on the encoding it into bytes). This will first put an
8 | * int with the length of the bytearray, and then the bytearray itself.
9 | *
10 | * Length = -1 is used to store null
11 | */
12 | fun ByteBuffer.putStringFromBytes(
13 | strBytes: ByteArray?
14 | ): ByteBuffer {
15 | if(strBytes != null) {
16 | putInt(strBytes.size)
17 | put(strBytes)
18 | }else {
19 | putInt(-1)
20 | }
21 | return this
22 | }
23 |
24 | /**
25 | * Get a string that was stored using putStringFromBytes
26 | */
27 | fun ByteBuffer.getString(): String? {
28 | val len = int
29 | if(len != -1) {
30 | val strBytes = ByteArray(len)
31 | get(strBytes)
32 | return String(strBytes)
33 | }else {
34 | return null
35 | }
36 | }
37 |
38 | fun ByteBuffer.putBoolean(boolean: Boolean) : ByteBuffer {
39 | return put(if(boolean) 1 else 0)
40 | }
41 |
42 | fun ByteBuffer.getBoolean() : Boolean {
43 | return get() != 0.toByte()
44 | }
45 |
46 | fun ByteBuffer.getStringOrThrow() : String {
47 | return getString() ?: throw NullPointerException("ByteBuffer.getStringOrThrow: stored string was null")
48 | }
49 |
50 | fun ByteBuffer.putInet4Address(inetAddress: InetAddress): ByteBuffer {
51 | val addressBytes = inetAddress.address
52 | if(addressBytes.size != 4)
53 | throw IllegalArgumentException("putInetAddr: expected address of 4 bytes got ${addressBytes.size}")
54 |
55 | put(inetAddress.address)
56 |
57 | return this
58 | }
59 |
60 | fun ByteBuffer.getInet4Address(): InetAddress {
61 | val addressBytes = ByteArray(4)
62 | get(addressBytes)
63 | return InetAddress.getByAddress(addressBytes)
64 | }
65 |
66 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/HotspotType.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import kotlinx.serialization.KSerializer
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.descriptors.PrimitiveKind
6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 | @Serializable(with = HotspotTypeSerializer::class)
11 | enum class HotspotType(val flag: Byte) {
12 | LOCALONLY_HOTSPOT(1), WIFIDIRECT_GROUP(2), AUTO(4);
13 |
14 | override fun toString(): String {
15 | return when(this) {
16 | LOCALONLY_HOTSPOT -> "Local Only"
17 | WIFIDIRECT_GROUP -> "WiFi Direct"
18 | AUTO -> "Auto"
19 | }
20 | }
21 |
22 | companion object {
23 | fun fromFlag(flag: Byte): HotspotType {
24 | return values().first { it.flag == flag }
25 | }
26 |
27 | /**
28 | * Normally the system will determine what type of hotspot should be created (wifi direct)
29 | * or local only. However the user might override this.
30 | */
31 | fun forceTypeIfSpecified(
32 | specifiedType: HotspotType,
33 | autoType: HotspotType?,
34 | ): HotspotType? {
35 | return if(specifiedType != AUTO) {
36 | specifiedType
37 | }else {
38 | autoType
39 | }
40 | }
41 |
42 | }
43 | }
44 |
45 | object HotspotTypeSerializer: KSerializer {
46 | override fun deserialize(decoder: Decoder): HotspotType {
47 | return HotspotType.fromFlag(decoder.decodeByte())
48 | }
49 |
50 | override val descriptor: SerialDescriptor
51 | get() = PrimitiveSerialDescriptor("hotspotType", PrimitiveKind.BYTE)
52 |
53 | override fun serialize(encoder: Encoder, value: HotspotType) {
54 | encoder.encodeByte(value.flag)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/viewmodel/SendFileViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.viewmodel
2 |
3 | import android.net.Uri
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
7 | import com.ustadmobile.meshrabiya.testapp.server.TestAppServer
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.asStateFlow
11 | import kotlinx.coroutines.flow.update
12 | import kotlinx.coroutines.launch
13 | import org.kodein.di.DI
14 | import org.kodein.di.instance
15 | import java.net.InetAddress
16 |
17 |
18 | data class SendFileUiState(
19 | val pendingTransfers: List = emptyList(),
20 | val appUiState: AppUiState = AppUiState(),
21 | )
22 |
23 | //Screen is essentially a list of pending transfers with a FAB to send a file. Clicking the fab triggers
24 | //the file selector, then selecting a recipient.
25 | class SendFileViewModel(
26 | di: DI,
27 | private val onNavigateToSelectReceiveNode: (Uri) -> Unit,
28 | ) : ViewModel(){
29 |
30 | private val _uiState = MutableStateFlow(SendFileUiState())
31 |
32 | val uiState: Flow = _uiState.asStateFlow()
33 |
34 | private val testAppServer: TestAppServer by di.instance()
35 |
36 | init {
37 | _uiState.update { prev ->
38 | prev.copy(
39 | appUiState = AppUiState(
40 | title = "Send"
41 | )
42 | )
43 | }
44 |
45 | viewModelScope.launch {
46 | testAppServer.outgoingTransfers.collect {
47 | _uiState.update { prev ->
48 | prev.copy(
49 | pendingTransfers = it,
50 | )
51 | }
52 | }
53 | }
54 | }
55 |
56 | fun onSelectFileToSend(
57 | uri: Uri?,
58 | ) {
59 | if(uri == null)
60 | return
61 |
62 | onNavigateToSelectReceiveNode(uri)
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/mmcp/MmcpHotspotRequest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import com.ustadmobile.meshrabiya.vnet.wifi.ConnectBand
4 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotType
5 | import com.ustadmobile.meshrabiya.vnet.wifi.LocalHotspotRequest
6 | import java.nio.ByteBuffer
7 | import java.nio.ByteOrder
8 |
9 | class MmcpHotspotRequest(
10 | messageId: Int,
11 | val hotspotRequest: LocalHotspotRequest,
12 | ): MmcpMessage(WHAT_HOTSPOT_REQUEST, messageId) {
13 |
14 |
15 | override fun toBytes(): ByteArray {
16 | return headerAndPayloadToBytes(header,
17 | ByteBuffer.wrap(ByteArray(2))
18 | .put(hotspotRequest.preferredBand.flag)
19 | .put(hotspotRequest.preferredType.flag)
20 | .array()
21 | )
22 | }
23 |
24 | override fun equals(other: Any?): Boolean {
25 | if (this === other) return true
26 | if (other !is MmcpHotspotRequest) return false
27 | if (!super.equals(other)) return false
28 |
29 | if (hotspotRequest != other.hotspotRequest) return false
30 |
31 | return true
32 | }
33 |
34 | override fun hashCode(): Int {
35 | var result = super.hashCode()
36 | result = 31 * result + hotspotRequest.hashCode()
37 | return result
38 | }
39 |
40 | companion object {
41 |
42 | fun fromBytes(
43 | byteArray: ByteArray,
44 | offset: Int = 0,
45 | len: Int = byteArray.size,
46 | ): MmcpHotspotRequest {
47 | val (header, payload) = mmcpHeaderAndPayloadFromBytes(
48 | byteArray, offset, len
49 | )
50 |
51 | val byteBuffer = ByteBuffer.wrap(payload)
52 | .order(ByteOrder.BIG_ENDIAN)
53 |
54 | val preferredBand = ConnectBand.fromFlag(byteBuffer.get())
55 | val preferredHotspotType = HotspotType.fromFlag(byteBuffer.get())
56 |
57 | return MmcpHotspotRequest(
58 | messageId = header.messageId,
59 | hotspotRequest = LocalHotspotRequest(preferredBand, preferredHotspotType)
60 | )
61 | }
62 |
63 | }
64 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/quic/CertGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.quic
2 |
3 | import org.bouncycastle.asn1.x500.X500Name
4 | import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
5 | import org.bouncycastle.cert.X509v3CertificateBuilder
6 | import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
7 | import org.bouncycastle.jce.provider.BouncyCastleProvider
8 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
9 | import java.math.BigInteger
10 | import java.security.KeyPair
11 | import java.security.KeyPairGenerator
12 | import java.security.SecureRandom
13 | import java.security.cert.X509Certificate
14 | import java.util.Calendar
15 | import java.util.Date
16 |
17 |
18 | //See https://gist.github.com/alessandroleite/fa3e763552bb8b409bfa
19 | // See also: https://www.programcreek.com/java-api-examples/?api=org.bouncycastle.cert.X509v3CertificateBuilder
20 |
21 | fun generateKeyPair() : KeyPair {
22 | val keyGenerator = KeyPairGenerator.getInstance("RSA")
23 | keyGenerator.initialize(2048, SecureRandom())
24 | val keyPair = keyGenerator.generateKeyPair()
25 |
26 | return keyPair
27 | }
28 |
29 | fun generateX509Cert(
30 | keyPair: KeyPair,
31 | startDate: Date = Date(),
32 | endDate: Date = Calendar.getInstance().let {
33 | it.set(Calendar.YEAR, it.get(Calendar.YEAR) + 10)
34 |
35 | Date(it.timeInMillis)
36 | },
37 | issuerName: X500Name = X500Name("CN=Meshrabiya"),
38 | subjectName: X500Name = X500Name("CN=Meshrabiya, OU=Mesh Net, O=UstadMobile FZLLC, L=Dubai, C=AE"),
39 | ) : X509Certificate{
40 | val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.public.encoded)
41 | val certBuilder = X509v3CertificateBuilder(
42 | issuerName,
43 | BigInteger.valueOf(System.currentTimeMillis()),
44 | startDate,
45 | endDate,
46 | subjectName,
47 | subjectPublicKeyInfo
48 | )
49 | val signer = JcaContentSignerBuilder("SHA256WithRSA")
50 | .setProvider(BouncyCastleProvider())
51 | .build(keyPair.private)
52 |
53 | val certHolder = certBuilder.build(signer)
54 | val cert = JcaX509CertificateConverter()
55 | .setProvider(BouncyCastleProvider())
56 | .getCertificate(certHolder)
57 |
58 | return cert
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/wifi/WifiConnectConfigTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import com.ustadmobile.meshrabiya.ext.requireAsIpv6
4 | import com.ustadmobile.meshrabiya.vnet.randomApipaAddr
5 | import kotlinx.serialization.json.Json
6 | import org.junit.Assert
7 | import org.junit.Test
8 | import java.net.Inet6Address
9 |
10 | class WifiConnectConfigTest {
11 |
12 | @Test
13 | fun givenHotspotConfigWithSsidAndPassphrase_whenConvertedToAndFromBytes_thenWillBeEqual() {
14 | val hotspotConfig = WifiConnectConfig(
15 | nodeVirtualAddr = randomApipaAddr(),
16 | ssid = "test",
17 | passphrase = "secret",
18 | port = 8042,
19 | band = ConnectBand.BAND_5GHZ,
20 | hotspotType = HotspotType.LOCALONLY_HOTSPOT,
21 | linkLocalAddr = Inet6Address.getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
22 | .requireAsIpv6(),
23 | )
24 |
25 | val someOffset = 5//just to test that the offset is used appropriately
26 |
27 | val byteArr = ByteArray(hotspotConfig.sizeInBytes + someOffset)
28 | hotspotConfig.toBytes(byteArr, someOffset)
29 | val hotspotConfigFromBytes = WifiConnectConfig.fromBytes(byteArr, someOffset)
30 |
31 | Assert.assertEquals(hotspotConfig, hotspotConfigFromBytes)
32 | }
33 |
34 | @Test
35 | fun givenHotspotConfigSerialized_whenSerialized_thenWillMatch() {
36 | val hotspotConfig = WifiConnectConfig(
37 | nodeVirtualAddr = randomApipaAddr(),
38 | ssid = "test",
39 | passphrase = "secret",
40 | port = 8042,
41 | band = ConnectBand.BAND_5GHZ,
42 | hotspotType = HotspotType.LOCALONLY_HOTSPOT,
43 | linkLocalAddr = Inet6Address
44 | .getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334").requireAsIpv6(),
45 | )
46 |
47 | val json = Json {
48 | encodeDefaults = true
49 | }
50 |
51 | val configJsonStr = json.encodeToString(WifiConnectConfig.serializer(), hotspotConfig)
52 |
53 | val hotspotConfigFromJson = json.decodeFromString(
54 | WifiConnectConfig.serializer(), configJsonStr
55 | )
56 |
57 | Assert.assertEquals(hotspotConfig, hotspotConfigFromJson)
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/viewmodel/LogListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.ustadmobile.meshrabiya.testapp.App.Companion.TAG_LOG_DIR
6 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.asStateFlow
11 | import kotlinx.coroutines.flow.update
12 | import kotlinx.coroutines.launch
13 | import org.kodein.di.DI
14 | import org.kodein.di.instance
15 | import java.io.File
16 |
17 | data class LogFile(
18 | val file: File,
19 | val size: Long,
20 | val lastModified: Long,
21 | )
22 |
23 | data class LogListUiState(
24 | val logFiles: List = emptyList(),
25 | val appUiState: AppUiState = AppUiState(),
26 | )
27 |
28 | class LogListViewModel(
29 | di: DI
30 | ) : ViewModel() {
31 |
32 | private val _uiState = MutableStateFlow(
33 | LogListUiState(
34 | appUiState = AppUiState(
35 | title = "Logs"
36 | )
37 | )
38 | )
39 |
40 | val uiState: Flow = _uiState.asStateFlow()
41 |
42 | val logDir: File by di.instance(tag = TAG_LOG_DIR)
43 |
44 | init {
45 | viewModelScope.launch(Dispatchers.IO) {
46 | val logFiles: List = (logDir.listFiles()?.toList() ?: emptyList())
47 | .map {
48 | LogFile(
49 | file = it,
50 | size = it.length(),
51 | lastModified = it.lastModified(),
52 | )
53 | }.sortedByDescending { it.lastModified }
54 |
55 | _uiState.update { prev ->
56 | prev.copy(logFiles = logFiles)
57 | }
58 | }
59 | }
60 |
61 | fun onClickDelete(logFile: LogFile) {
62 | viewModelScope.launch(Dispatchers.IO) {
63 | if(logFile.file.delete()) {
64 | _uiState.update { prev ->
65 | prev.copy(
66 | logFiles = prev.logFiles.filter { it.file != logFile.file }
67 | )
68 | }
69 | }
70 | }
71 | }
72 |
73 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/HotspotPersistenceType.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import kotlinx.serialization.KSerializer
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.descriptors.PrimitiveKind
6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 |
11 | /**
12 | * Flag that indicates if a Hotspot is going to be persistent (e.g. will keep the same BSSID, SSID,
13 | * etc. for future connect attempts).
14 | *
15 | * If the BSSID is probably persistent, but other details are not, we might want to use
16 | * CompanionDeviceManager on Android 11+ to avoid a prompt for the user when reconnecting.
17 | */
18 | @Serializable(with = HotspotPersistenceTypeSerializer::class)
19 | enum class HotspotPersistenceType(val flag: Byte) {
20 |
21 | /**
22 | * The hotspot is not persistent at all. This is the behavior of LocalOnlyHotspot on
23 | * Android 11 and 12 where the BSSID is randomized each time it is created and (probably) for
24 | * WifiDirect groups on Android 9 and below.
25 | */
26 | NONE(0),
27 |
28 | /**
29 | * The BSSID is probably going to stay the same. This is the behavior of LocalOnlyHotspot on
30 | * Android 10 and prior. The BSSID will stay the same unless the device is restarted.
31 | */
32 | PROBABLY_BSSID(1),
33 |
34 | /**
35 | * Everything is fully persistent (e.g. we set it). This is the behavior of LocalOnlyHotspot on
36 | * Android 13+ and for WifiDirect Groups on Android 10+
37 | */
38 | FULL(2);
39 |
40 |
41 | companion object {
42 | fun fromFlag(flag: Byte): HotspotPersistenceType {
43 | return HotspotPersistenceType.values().first { it.flag == flag }
44 | }
45 | }
46 | }
47 |
48 | object HotspotPersistenceTypeSerializer: KSerializer {
49 | override fun deserialize(decoder: Decoder): HotspotPersistenceType {
50 | return HotspotPersistenceType.fromFlag(decoder.decodeByte())
51 | }
52 |
53 | override val descriptor: SerialDescriptor
54 | get() = PrimitiveSerialDescriptor("hotspotPersistenceType", PrimitiveKind.BYTE)
55 |
56 | override fun serialize(encoder: Encoder, value: HotspotPersistenceType) {
57 | encoder.encodeByte(value.flag)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun HttpOverBluetoothTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/state/MeshrabiyaWifiState.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi.state
2 |
3 | import com.ustadmobile.meshrabiya.vnet.WifiRole
4 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
5 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotStatus
6 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotType
7 |
8 |
9 | data class MeshrabiyaWifiState(
10 | val wifiRole: WifiRole = WifiRole.NONE,
11 | val wifiDirectState: WifiDirectState = WifiDirectState(),
12 | val wifiStationState: WifiStationState = WifiStationState(),
13 | val localOnlyHotspotState: LocalOnlyHotspotState = LocalOnlyHotspotState(),
14 | val errorCode: Int = 0,
15 | val concurrentApStationSupported: Boolean = false,
16 | ) {
17 |
18 | /**
19 | * The configuration that another device should use to connect to this device (if any)
20 | */
21 | val connectConfig: WifiConnectConfig?
22 | get() = wifiDirectState.config ?: localOnlyHotspotState.config
23 |
24 | val hotspotIsStarting: Boolean
25 | get() = wifiDirectState.hotspotStatus == HotspotStatus.STARTING
26 | || localOnlyHotspotState.status == HotspotStatus.STARTING
27 |
28 | val hotspotIsStarted: Boolean
29 | get() = wifiDirectState.hotspotStatus == HotspotStatus.STARTED
30 | || localOnlyHotspotState.status == HotspotStatus.STARTED
31 |
32 | fun hotspotError(hotspotType: HotspotType) : Int {
33 | return when(hotspotType) {
34 | HotspotType.LOCALONLY_HOTSPOT -> localOnlyHotspotState.error
35 | HotspotType.WIFIDIRECT_GROUP -> wifiDirectState.error
36 | HotspotType.AUTO -> 0
37 | }
38 | }
39 |
40 |
41 | /**
42 | * Determine the type of hotspot that should be created if a request is made to start one.
43 | * Currently only WifiDirect group is supported.
44 | */
45 | val hotspotTypeToCreate: HotspotType?
46 | get() {
47 | return if(connectConfig != null)
48 | //Hotspot already available- nothing to create
49 | null
50 | else if(
51 | //WifiDirect Group or Local Only hotspot already being created, do nothing
52 | hotspotIsStarting
53 | ) {
54 | null
55 | } else if(concurrentApStationSupported){
56 | HotspotType.LOCALONLY_HOTSPOT
57 | }else {
58 | HotspotType.WIFIDIRECT_GROUP
59 | }
60 |
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/portforward/ForwardingTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.portforward
2 |
3 | import com.ustadmobile.meshrabiya.log.MNetLoggerStdout
4 | import com.ustadmobile.meshrabiya.test.EchoDatagramServer
5 | import org.junit.Assert
6 | import org.junit.Test
7 | import java.net.DatagramPacket
8 | import java.net.DatagramSocket
9 | import java.net.InetAddress
10 | import java.util.concurrent.Executors
11 |
12 | class ForwardingTest {
13 |
14 | @Test(timeout = 5000)
15 | fun givenEchoSent_whenListening_willReceive() {
16 | val executor = Executors.newCachedThreadPool()
17 | val echoServer = EchoDatagramServer(0, executor)
18 |
19 | val client = DatagramSocket()
20 |
21 | val helloBytes = "Hello".toByteArray()
22 | val helloPacket = DatagramPacket(helloBytes, helloBytes.size,
23 | InetAddress.getLoopbackAddress(), echoServer.listeningPort)
24 | client.send(helloPacket)
25 |
26 | val receiveBuffer = ByteArray(100)
27 | val receivePacket = DatagramPacket(receiveBuffer, receiveBuffer.size)
28 | client.receive(receivePacket)
29 |
30 | val decoded = String(receivePacket.data, receivePacket.offset, receivePacket.length)
31 | Assert.assertEquals("Hello", decoded)
32 | executor.shutdown()
33 | echoServer.close()
34 | }
35 |
36 | @Test(timeout = 5000)
37 | fun givenPortForwardingRuleActive_whenPacketSentToForwarder_thenReplyWillBeReceived() {
38 | val executor = Executors.newCachedThreadPool()
39 | val echoServer = EchoDatagramServer(0, executor)
40 |
41 | val forwardRuleDatagramSocket = DatagramSocket()
42 | val forwardingRule = UdpForwardRule(
43 | DatagramSocket(), executor,
44 | InetAddress.getLoopbackAddress(), echoServer.listeningPort,
45 | logger = MNetLoggerStdout()
46 | )
47 |
48 | val client = DatagramSocket()
49 | val helloBytes = "Hello".toByteArray()
50 | val helloPacket = DatagramPacket(helloBytes, helloBytes.size,
51 | forwardRuleDatagramSocket.localAddress, forwardingRule.localPort)
52 | client.send(helloPacket)
53 |
54 | val receiveBuffer = ByteArray(100)
55 | val receivePacket = DatagramPacket(receiveBuffer, receiveBuffer.size)
56 | client.receive(receivePacket)
57 |
58 | val decoded = String(receivePacket.data, receivePacket.offset, receivePacket.length)
59 | Assert.assertEquals("Hello", decoded)
60 | executor.shutdown()
61 | echoServer.close()
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/wifi/LocalHotspotResponse.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.wifi
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 |
6 | data class LocalHotspotResponse(
7 | val responseToMessageId: Int,
8 | val errorCode: Int,
9 | val config: WifiConnectConfig?,
10 | val redirectAddr: Int,
11 | ) {
12 |
13 | val sizeInBytes: Int
14 | //Size = responseToMessageId + errorCode + (1 byte indicating if there is a config) + config bytes + redirect addr
15 | get() = 4 + 4 + 1 + (config?.sizeInBytes ?: 0) + 4
16 |
17 | fun toBytes(): ByteArray {
18 | return ByteArray(sizeInBytes).also {
19 | toBytes(it, 0)
20 | }
21 | }
22 |
23 | fun toBytes(
24 | byteArray: ByteArray,
25 | offset: Int
26 | ) {
27 | val byteBuf = ByteBuffer.wrap(byteArray, offset, byteArray.size - offset)
28 | .order(ByteOrder.BIG_ENDIAN)
29 | byteBuf.putInt(responseToMessageId)
30 | byteBuf.putInt(errorCode)
31 | byteBuf.put(if(config != null) 1.toByte() else 0.toByte())
32 | if(config != null) {
33 | val configOffset = offset + CONFIG_OFFSET
34 | val configSize = config.toBytes(byteArray, configOffset)
35 | byteBuf.position(byteBuf.position() + configSize)
36 | }
37 | byteBuf.putInt(redirectAddr)
38 | }
39 |
40 |
41 |
42 | companion object {
43 |
44 | fun fromBytes(
45 | byteArray: ByteArray,
46 | offset: Int
47 | ): LocalHotspotResponse {
48 | val byteBuf = ByteBuffer.wrap(byteArray, offset, byteArray.size - offset)
49 | .order(ByteOrder.BIG_ENDIAN)
50 | val responseToMessageId = byteBuf.int
51 | val errorCode = byteBuf.int
52 | val hasHotspotConfig = byteBuf.get() != 0.toByte()
53 | val config = if(hasHotspotConfig) {
54 | WifiConnectConfig.fromBytes(byteArray, offset + CONFIG_OFFSET).also {
55 | byteBuf.position(byteBuf.position() + it.sizeInBytes)
56 | }
57 | }else {
58 | null
59 | }
60 | val redirectAddr = byteBuf.int
61 |
62 | return LocalHotspotResponse(
63 | responseToMessageId = responseToMessageId,
64 | errorCode = errorCode,
65 | config = config,
66 | redirectAddr = redirectAddr,
67 | )
68 | }
69 |
70 | //offset = 9 (4 bytes for responseToMessageId, 4 bytes for errorcode, 1 byte indicating if there is or is not a config)
71 | //This must be incremented if other content is added before the config
72 | private const val CONFIG_OFFSET = 9
73 |
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/test-app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | # As per missing_rules.txt - no idea why these are being referenced via ACRA etc (see deps - com.google.auto.service:auto-service)
24 | -dontwarn javax.annotation.processing.AbstractProcessor
25 | -dontwarn javax.annotation.processing.SupportedOptions
26 | -dontwarn javax.crypto.spec.ChaCha20ParameterSpec
27 |
28 | # Kodein as per https://kosi-libs.org/kodein/7.19/framework/android.html#_proguard_configuration
29 | -keep, allowobfuscation, allowoptimization class org.kodein.type.TypeReference
30 | -keep, allowobfuscation, allowoptimization class org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest
31 |
32 | -keep, allowobfuscation, allowoptimization class * extends org.kodein.type.TypeReference
33 | -keep, allowobfuscation, allowoptimization class * extends org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest
34 |
35 | # Bouncycastle
36 | -keep class org.bouncycastle.jcajce.provider.** { *; }
37 | -keep class org.bouncycastle.jce.provider.** { *; }
38 |
39 | -dontwarn javax.naming.**
40 |
41 | #Try remove debug log calls
42 | -assumenosideeffects interface net.luminis.quic.log.Logger {
43 | void logDebug(...);
44 | void logRaw(...);
45 | void logDecrypted(...);
46 | void debug(...);
47 | void debugWithHexBlock(...);
48 | void received(...);
49 | void sent(...);
50 | void raw(...);
51 | void decrypted(...);
52 | void encrypted(...);
53 | void receivedPacketInfo(...);
54 | void sentPacketInfo(...);
55 | }
56 |
57 |
58 | # As per Android Gradle Plugin 8 (2/May/2023) - probably OKHTTP related
59 | -dontwarn org.bouncycastle.jsse.BCSSLParameters
60 | -dontwarn org.bouncycastle.jsse.BCSSLSocket
61 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
62 | -dontwarn org.conscrypt.Conscrypt$Version
63 | -dontwarn org.conscrypt.Conscrypt
64 | -dontwarn org.conscrypt.ConscryptHostnameVerifier
65 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters
66 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket
67 | -dontwarn org.openjsse.net.ssl.OpenJSSE
68 | -dontwarn org.slf4j.impl.StaticLoggerBinder
69 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.Context
6 | import android.content.ContextWrapper
7 | import android.content.pm.PackageManager
8 | import android.net.wifi.WifiManager
9 | import android.os.Build
10 | import androidx.core.content.ContextCompat
11 | import androidx.datastore.core.DataStore
12 | import androidx.datastore.preferences.core.Preferences
13 | import androidx.datastore.preferences.preferencesDataStore
14 | import com.ustadmobile.meshrabiya.MeshrabiyaConstants
15 |
16 | fun Context.getActivityContext(): Activity = when (this) {
17 | is Activity -> this
18 | is ContextWrapper -> this.baseContext.getActivityContext()
19 | else -> throw IllegalArgumentException("Not an activity context")
20 | }
21 |
22 | /**
23 | * On Android 13+ we can use the NEARBY_WIFI_DEVICES permission instead of the location permission.
24 | * On earlier versions, we need fine location permission
25 | */
26 | val NEARBY_WIFI_PERMISSION_NAME = if(Build.VERSION.SDK_INT >= 33){
27 | Manifest.permission.NEARBY_WIFI_DEVICES
28 | }else {
29 | Manifest.permission.ACCESS_FINE_LOCATION
30 | }
31 |
32 |
33 | fun Context.hasNearbyWifiDevicesOrLocationPermission(): Boolean {
34 | return ContextCompat.checkSelfPermission(
35 | this, NEARBY_WIFI_PERMISSION_NAME
36 | ) == PackageManager.PERMISSION_GRANTED
37 | }
38 |
39 | fun Context.hasBluetoothConnectPermission(): Boolean {
40 | return if(Build.VERSION.SDK_INT >= 31) {
41 | ContextCompat.checkSelfPermission(
42 | this, Manifest.permission.BLUETOOTH_CONNECT
43 | ) == PackageManager.PERMISSION_GRANTED
44 | }else {
45 | true
46 | }
47 | }
48 |
49 | val Context.dataStore: DataStore by preferencesDataStore(name = "meshr_settings")
50 |
51 | fun Context.meshrabiyaDeviceInfoStr(): String {
52 | val wifiManager = getSystemService(WifiManager::class.java)
53 | val hasStaConcurrency = Build.VERSION.SDK_INT >= 31 &&
54 | wifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported
55 | val hasStaApConcurrency = Build.VERSION.SDK_INT >= 30 &&
56 | wifiManager.isStaApConcurrencySupported
57 | val hasWifiAwareSupport = packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)
58 |
59 | return buildString {
60 | append("Meshrabiya: Version :${MeshrabiyaConstants.VERSION}\n")
61 | append("Android Version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})\n")
62 | append("Device: ${Build.MANUFACTURER} - ${Build.MODEL}\n")
63 | append("5Ghz supported: ${wifiManager.is5GHzBandSupported}\n")
64 | append("Local-only station concurrency: $hasStaConcurrency\n")
65 | append("Station-AP concurrency: $hasStaApConcurrency\n")
66 | append("WifiAware support: $hasWifiAwareSupport\n")
67 | }
68 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/VirtualNodeTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import app.cash.turbine.test
4 | import com.ustadmobile.meshrabiya.log.MNetLoggerStdout
5 | import com.ustadmobile.meshrabiya.ext.addressToByteArray
6 | import com.ustadmobile.meshrabiya.ext.addressToDotNotation
7 | import com.ustadmobile.meshrabiya.ext.ip4AddressToInt
8 | import com.ustadmobile.meshrabiya.ext.requireAsIpv6
9 | import com.ustadmobile.meshrabiya.mmcp.MmcpHotspotRequest
10 | import com.ustadmobile.meshrabiya.mmcp.MmcpHotspotResponse
11 | import com.ustadmobile.meshrabiya.mmcp.MmcpMessage
12 | import com.ustadmobile.meshrabiya.mmcp.MmcpPing
13 | import com.ustadmobile.meshrabiya.mmcp.MmcpPong
14 | import com.ustadmobile.meshrabiya.test.EchoDatagramServer
15 | import com.ustadmobile.meshrabiya.test.TestVirtualNode
16 | import com.ustadmobile.meshrabiya.test.assertByteArrayEquals
17 | import com.ustadmobile.meshrabiya.test.connectTo
18 | import com.ustadmobile.meshrabiya.vnet.VirtualPacket.Companion.ADDR_BROADCAST
19 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
20 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotType
21 | import com.ustadmobile.meshrabiya.vnet.wifi.MeshrabiyaWifiManager
22 | import com.ustadmobile.meshrabiya.vnet.wifi.LocalHotspotRequest
23 | import com.ustadmobile.meshrabiya.vnet.wifi.LocalHotspotResponse
24 | import com.ustadmobile.meshrabiya.vnet.wifi.state.MeshrabiyaWifiState
25 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotStatus
26 | import com.ustadmobile.meshrabiya.vnet.wifi.state.WifiDirectState
27 | import kotlinx.coroutines.CoroutineScope
28 | import kotlinx.coroutines.Dispatchers
29 | import kotlinx.coroutines.Job
30 | import kotlinx.coroutines.async
31 | import kotlinx.coroutines.awaitAll
32 | import kotlinx.coroutines.cancel
33 | import kotlinx.coroutines.flow.MutableStateFlow
34 | import kotlinx.coroutines.flow.filter
35 | import kotlinx.coroutines.flow.first
36 | import kotlinx.coroutines.runBlocking
37 | import kotlinx.serialization.json.Json
38 | import org.junit.Assert
39 | import org.junit.Test
40 | import org.mockito.kotlin.any
41 | import org.mockito.kotlin.eq
42 | import org.mockito.kotlin.mock
43 | import org.mockito.kotlin.timeout
44 | import org.mockito.kotlin.verifyBlocking
45 | import java.net.DatagramPacket
46 | import java.net.DatagramSocket
47 | import java.net.Inet6Address
48 | import java.net.InetAddress
49 | import java.net.InetSocketAddress
50 | import java.util.UUID
51 | import java.util.concurrent.CountDownLatch
52 | import java.util.concurrent.Executors
53 | import java.util.concurrent.TimeUnit
54 | import java.util.concurrent.atomic.AtomicReference
55 | import kotlin.random.Random
56 | import kotlin.time.Duration.Companion.milliseconds
57 | import kotlin.time.Duration.Companion.seconds
58 |
59 | class VirtualNodeTest {
60 |
61 |
62 | private val logger = MNetLoggerStdout()
63 |
64 | private val json = Json {
65 | encodeDefaults = true
66 | }
67 |
68 |
69 |
70 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/socket/ChainSocket.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.socket
2 |
3 | import android.util.Log
4 | import com.ustadmobile.meshrabiya.ext.prefixMatches
5 | import com.ustadmobile.meshrabiya.log.MNetLogger
6 | import com.ustadmobile.meshrabiya.vnet.VirtualRouter
7 | import java.net.InetSocketAddress
8 | import java.net.Socket
9 | import java.net.SocketAddress
10 |
11 | /**
12 | * This is a replacement for the no-args Socket constructor, where the destination is not known
13 | * when the socket is constructed.
14 | *
15 | * If the intended address is provided at the time of construction, we can't override the connect
16 | * function because connect gets called as part of the superclass initialization. At the time of
17 | * superclass initialization, child class variables (e.g. the virtual router) will not be initialized.
18 | *
19 | * It would be possible to use extension functions to adjust the call to super initialization, but
20 | * it would not be possible to determine if this was the final hop (or not).
21 | */
22 | class ChainSocket(
23 | private val virtualRouter: VirtualRouter,
24 | private val logger: MNetLogger,
25 | ): Socket() {
26 |
27 | private val logPrefix = "[ChainSocket for ${virtualRouter.address}]"
28 |
29 | override fun connect(endpoint: SocketAddress, timeout: Int) {
30 | val endpointInetAddr = endpoint as? InetSocketAddress
31 | val address = endpointInetAddr?.address
32 | if(
33 | address?.prefixMatches(
34 | virtualRouter.networkPrefixLength, virtualRouter.address
35 | ) == true
36 | ) {
37 | val nextHop = virtualRouter.lookupNextHopForChainSocket(
38 | endpointInetAddr.address, endpoint.port
39 | )
40 |
41 | val network = nextHop.network
42 | if(network != null) {
43 | logger(Log.DEBUG, "$logPrefix binding socket to network $network to connect to $endpoint")
44 | network.bindSocket(this)
45 | }
46 |
47 | try {
48 | super.connect(InetSocketAddress(nextHop.address, nextHop.port))
49 |
50 | initializeChainIfNotFinalDest(
51 | ChainSocketInitRequest(
52 | virtualDestAddr = address,
53 | virtualDestPort = endpointInetAddr.port,
54 | fromAddr = virtualRouter.address
55 | ),
56 | nextHop
57 | )
58 | logger(
59 | Log.INFO, "$logPrefix created socket to $address:$port " +
60 | "nexthop = ${nextHop.address}:${nextHop.port}")
61 | }catch(e: Exception) {
62 | logger(Log.ERROR, "$logPrefix Exception connecting to $endpoint " +
63 | "nexthop=${nextHop.address}:${nextHop.port} (finalDest=${nextHop.isFinalDest})", e)
64 | throw e
65 | }
66 | }else {
67 | super.connect(endpoint, timeout)
68 | }
69 |
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/VirtualNodeDatagramSocketTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import com.ustadmobile.meshrabiya.log.MNetLoggerStdout
4 | import org.junit.Test
5 | import org.mockito.kotlin.any
6 | import org.mockito.kotlin.argWhere
7 | import org.mockito.kotlin.eq
8 | import org.mockito.kotlin.mock
9 | import org.mockito.kotlin.timeout
10 | import org.mockito.kotlin.verify
11 | import java.net.DatagramSocket
12 | import java.net.InetAddress
13 | import java.util.concurrent.Executors
14 | import kotlin.random.Random
15 |
16 | class VirtualNodeDatagramSocketTest {
17 |
18 | @Test
19 | fun givenTwoVirtualNodeDatagramSockets_whenOneSendsVirtualPacketToOther_thenReceiveWillCallVirtualRouter() {
20 | val executorService = Executors.newCachedThreadPool()
21 | val socket1VirtualNodeAddr = 42
22 | val socket2VirtualNodeAddr = 43
23 |
24 | val socket1Router: VirtualRouter = mock { }
25 | val socket1 = VirtualNodeDatagramSocket(
26 | socket = DatagramSocket(0),
27 | localNodeVirtualAddress = socket1VirtualNodeAddr,
28 | ioExecutorService = executorService,
29 | router = socket1Router,
30 | logger = MNetLoggerStdout(),
31 | )
32 |
33 | val socket2Router: VirtualRouter = mock { }
34 | val socket2 = VirtualNodeDatagramSocket(
35 | socket = DatagramSocket(0),
36 | localNodeVirtualAddress = socket2VirtualNodeAddr,
37 | ioExecutorService = executorService,
38 | router = socket2Router,
39 | logger = MNetLoggerStdout(),
40 | )
41 |
42 | try {
43 | val data = Random.nextBytes(ByteArray(1000 + VirtualPacketHeader.HEADER_SIZE))
44 | val packetToSend = VirtualPacket.fromHeaderAndPayloadData(
45 | header = VirtualPacketHeader(
46 | toAddr = socket2VirtualNodeAddr,
47 | toPort = 80,
48 | fromAddr = socket1VirtualNodeAddr,
49 | fromPort = 80,
50 | lastHopAddr = socket1VirtualNodeAddr,
51 | hopCount = 1.toByte(),
52 | maxHops = 8.toByte(),
53 | payloadSize = 1000
54 | ),
55 | data = data,
56 | payloadOffset = VirtualPacketHeader.HEADER_SIZE,
57 | )
58 |
59 | socket1.send(
60 | nextHopAddress = InetAddress.getLoopbackAddress(),
61 | nextHopPort = socket2.localPort,
62 | virtualPacket = packetToSend,
63 | )
64 |
65 | verify(socket2Router, timeout(5000)).route(
66 | packet = argWhere {
67 | it.header == packetToSend.header
68 | },
69 | datagramPacket = any(),
70 | virtualNodeDatagramSocket = eq(socket2),
71 | )
72 | }finally {
73 | executorService.shutdown()
74 | socket1.close()
75 | socket2.close()
76 | }
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/viewmodel/ReceiveViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.viewmodel
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.ustadmobile.meshrabiya.MeshrabiyaConstants.LOG_TAG
7 | import com.ustadmobile.meshrabiya.testapp.App
8 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
9 | import com.ustadmobile.meshrabiya.testapp.appstate.FabState
10 | import com.ustadmobile.meshrabiya.testapp.server.TestAppServer
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.flow.asStateFlow
15 | import kotlinx.coroutines.flow.update
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.withContext
18 | import org.kodein.di.DI
19 | import org.kodein.di.instance
20 | import java.io.File
21 |
22 | data class ReceiveUiState(
23 | val incomingTransfers: List = emptyList(),
24 | val appUiState: AppUiState = AppUiState(),
25 | )
26 |
27 | class ReceiveViewModel(
28 | private val di: DI
29 | ): ViewModel() {
30 |
31 | private val testAppServer: TestAppServer by di.instance()
32 |
33 | private val receiveDir: File by di.instance(tag = App.TAG_RECEIVE_DIR)
34 |
35 | private val _uiState = MutableStateFlow(ReceiveUiState())
36 |
37 | val uiState: Flow = _uiState.asStateFlow()
38 |
39 | init {
40 | _uiState.update {prev ->
41 | prev.copy(
42 | appUiState = AppUiState(
43 | title = "Receive",
44 | fabState = FabState(visible = false),
45 | )
46 | )
47 | }
48 |
49 | viewModelScope.launch {
50 | testAppServer.incomingTransfers.collect {
51 | _uiState.update { prev ->
52 | prev.copy(
53 | incomingTransfers = it
54 | )
55 | }
56 | }
57 | }
58 | }
59 |
60 | fun onClickAcceptIncomingTransfer(
61 | transfer: TestAppServer.IncomingTransfer
62 | ) {
63 | viewModelScope.launch {
64 | withContext(Dispatchers.IO) {
65 | receiveDir.takeIf { !it.exists() }?.mkdirs()
66 | val destFile = File(receiveDir, transfer.name)
67 | testAppServer.acceptIncomingTransfer(transfer, destFile)
68 | Log.i(LOG_TAG, "Received!! ${transfer.name} = ${destFile.length()} bytes")
69 |
70 | }
71 | }
72 | }
73 |
74 | fun onClickDeclineIncomingTransfer(
75 | transfer: TestAppServer.IncomingTransfer
76 | ) {
77 | viewModelScope.launch {
78 | testAppServer.onDeclineIncomingTransfer(transfer)
79 | }
80 | }
81 |
82 | fun onClickDeleteTransfer(
83 | transfer: TestAppServer.IncomingTransfer
84 | ) {
85 | viewModelScope.launch {
86 | testAppServer.onDeleteIncomingTransfer(transfer)
87 | }
88 | }
89 |
90 |
91 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/viewmodel/NeighborNodeListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
6 | import com.ustadmobile.meshrabiya.testapp.appstate.FabState
7 | import com.ustadmobile.meshrabiya.vnet.AndroidVirtualNode
8 | import com.ustadmobile.meshrabiya.vnet.VirtualNode
9 | import com.ustadmobile.meshrabiya.vnet.wifi.state.WifiStationState
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.flow.update
14 | import kotlinx.coroutines.launch
15 | import org.kodein.di.DI
16 | import org.kodein.di.instance
17 |
18 | data class NeighborNodeListUiState(
19 | val appUiState: AppUiState = AppUiState(),
20 | val filter: Filter = Filter.ALL_NODES,
21 | val connectingInProgressSsid: String? = null,
22 | internal val allNodes: Map = emptyMap(),
23 | ) {
24 |
25 | val nodes: Map
26 | get() {
27 | return if(filter == Filter.ALL_NODES) {
28 | allNodes
29 | }else {
30 | allNodes.filter { it.value.hopCount == 1.toByte() }
31 | }
32 | }
33 |
34 | companion object {
35 |
36 | enum class Filter(val label: String) {
37 | ALL_NODES("All"), NEIGHBORS("Neighbors")
38 | }
39 |
40 | }
41 | }
42 |
43 | class NeighborNodeListViewModel(
44 | di: DI
45 | ) : ViewModel(){
46 |
47 | private val _uiState = MutableStateFlow(NeighborNodeListUiState())
48 |
49 | val uiState: Flow = _uiState.asStateFlow()
50 |
51 | private val virtualNode: AndroidVirtualNode by di.instance()
52 |
53 |
54 | init {
55 | _uiState.update { prev ->
56 | prev.copy(
57 | appUiState = prev.appUiState.copy(
58 | title = "Network",
59 | fabState = FabState(
60 | visible = false,
61 | )
62 | )
63 | )
64 | }
65 |
66 | viewModelScope.launch {
67 | virtualNode.state.collect {
68 | _uiState.update { prev ->
69 | prev.copy(
70 | allNodes = it.originatorMessages,
71 | connectingInProgressSsid =
72 | if(it.wifiState.wifiStationState.status == WifiStationState.Status.CONNECTING) {
73 | it.wifiState.wifiStationState.config?.ssid
74 | }else {
75 | null
76 | },
77 | )
78 | }
79 | }
80 | }
81 | }
82 |
83 | fun onClickFilterChip(filter: NeighborNodeListUiState.Companion.Filter) {
84 | _uiState.update { prev ->
85 | prev.copy(
86 | filter = filter
87 | )
88 | }
89 | }
90 |
91 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/vnet/VirtualPacketTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 | import kotlin.random.Random
6 |
7 | class VirtualPacketTest {
8 |
9 | @Test
10 | fun givenVirtualPacket_whenConvertedToDatagramAndBackToVirtualPacket_thenShouldMatch() {
11 | val payloadSize = 1000
12 | val payload = Random.nextBytes(ByteArray(payloadSize + VirtualPacketHeader.HEADER_SIZE))
13 | val header = VirtualPacketHeader(
14 | toAddr = 1000,
15 | toPort = 8080,
16 | fromAddr = 1002,
17 | fromPort = 8072,
18 | lastHopAddr = 1002,
19 | hopCount = 1,
20 | maxHops = 4,
21 | payloadSize = payloadSize,
22 | )
23 |
24 | val virtualPacket = VirtualPacket.fromHeaderAndPayloadData(
25 | header = header,
26 | data = payload,
27 | payloadOffset = VirtualPacketHeader.HEADER_SIZE,
28 | )
29 |
30 | val datagramPacket = virtualPacket.toDatagramPacket()
31 | val virtualPacketFromDatagramPacket = VirtualPacket.fromDatagramPacket(datagramPacket)
32 |
33 | Assert.assertEquals(header, virtualPacketFromDatagramPacket.header)
34 | for(i in 0 until virtualPacket.header.payloadSize) {
35 | Assert.assertEquals(
36 | virtualPacket.data[i + virtualPacket.dataOffset],
37 | virtualPacketFromDatagramPacket.data[i + virtualPacketFromDatagramPacket.dataOffset]
38 | )
39 | }
40 | }
41 |
42 |
43 | @Test
44 | fun givenVirtualPacket_whenLastHopAddrSet_whenConvertedToDatagramAndBackToVirtualPacket_thenShouldMatch() {
45 | val payloadSize = 1000
46 | val payload = Random.nextBytes(ByteArray(payloadSize + VirtualPacketHeader.HEADER_SIZE))
47 | val header = VirtualPacketHeader(
48 | toAddr = 1000,
49 | toPort = 8080,
50 | fromAddr = 1002,
51 | fromPort = 8072,
52 | lastHopAddr = 0,
53 | hopCount = 1,
54 | maxHops = 4,
55 | payloadSize = payloadSize,
56 | )
57 |
58 | val virtualPacket = VirtualPacket.fromHeaderAndPayloadData(
59 | header = header,
60 | data = payload,
61 | payloadOffset = VirtualPacketHeader.HEADER_SIZE,
62 | )
63 | val lastHopAddr = 1042
64 | virtualPacket.updateLastHopAddrAndIncrementHopCountInData(lastHopAddr)
65 |
66 | val datagramPacket = virtualPacket.toDatagramPacket()
67 | val virtualPacketFromDatagramPacket = VirtualPacket.fromDatagramPacket(datagramPacket)
68 | Assert.assertEquals(virtualPacket.header.toAddr, virtualPacketFromDatagramPacket.header.toAddr)
69 |
70 | Assert.assertEquals(lastHopAddr, virtualPacketFromDatagramPacket.header.lastHopAddr)
71 | for(i in 0 until virtualPacket.header.payloadSize) {
72 | Assert.assertEquals(
73 | virtualPacket.data[i + virtualPacket.dataOffset],
74 | virtualPacketFromDatagramPacket.data[i + virtualPacketFromDatagramPacket.dataOffset]
75 | )
76 | }
77 | }
78 |
79 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/screens/SendFileScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.screens
2 |
3 | import android.net.Uri
4 | import androidx.activity.compose.rememberLauncherForActivityResult
5 | import androidx.activity.result.contract.ActivityResultContracts
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Send
10 | import androidx.compose.material3.ListItem
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.LaunchedEffect
14 | import androidx.compose.runtime.collectAsState
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
17 | import androidx.lifecycle.viewmodel.compose.viewModel
18 | import com.ustadmobile.meshrabiya.testapp.ViewModelFactory
19 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
20 | import com.ustadmobile.meshrabiya.testapp.appstate.FabState
21 | import com.ustadmobile.meshrabiya.testapp.viewmodel.SendFileUiState
22 | import com.ustadmobile.meshrabiya.testapp.viewmodel.SendFileViewModel
23 | import org.kodein.di.compose.localDI
24 |
25 | @Composable
26 | fun SendFileScreen(
27 | uiState: SendFileUiState,
28 | ){
29 | LazyColumn {
30 | items(
31 | items = uiState.pendingTransfers,
32 | key = { it.id }
33 | ) {transfer ->
34 | ListItem(
35 | headlineContent = {
36 | Text("${transfer.name} -> ${transfer.toHost.hostAddress}")
37 | },
38 | supportingContent = {
39 | Text("Status: ${transfer.status} Sent ${transfer.transferred} / ${transfer.size}")
40 | }
41 | )
42 | }
43 | }
44 | }
45 |
46 | @Composable
47 | fun SendFileScreen(
48 | onNavigateToSelectReceiveNode: (Uri) -> Unit,
49 | onSetAppUiState: (AppUiState) -> Unit,
50 | viewModel: SendFileViewModel = viewModel(
51 | factory = ViewModelFactory(
52 | di = localDI(),
53 | owner = LocalSavedStateRegistryOwner.current,
54 | vmFactory = {
55 | SendFileViewModel(it, onNavigateToSelectReceiveNode)
56 | },
57 | defaultArgs = null,
58 | )
59 | ),
60 | ) {
61 | val uiState: SendFileUiState by viewModel.uiState.collectAsState(SendFileUiState())
62 |
63 | val launcherPicker = rememberLauncherForActivityResult(
64 | contract = ActivityResultContracts.OpenDocument()
65 | ) { uri ->
66 | viewModel.onSelectFileToSend(uri)
67 | }
68 |
69 | LaunchedEffect(uiState.appUiState) {
70 | onSetAppUiState(uiState.appUiState.copy(
71 | fabState = FabState(
72 | visible = true,
73 | label = "Send File",
74 | icon = Icons.Default.Send,
75 | onClick = {
76 | launcherPicker.launch(arrayOf("*/*"))
77 | }
78 | )
79 | ))
80 | }
81 |
82 | SendFileScreen(
83 | uiState = uiState,
84 | )
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/test/java/com/ustadmobile/meshrabiya/mmcp/MmcpOriginatorMessageTest.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.mmcp
2 |
3 | import com.ustadmobile.meshrabiya.ext.requireAsIpv6
4 | import com.ustadmobile.meshrabiya.vnet.VirtualPacket.Companion.ADDR_BROADCAST
5 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotPersistenceType
6 | import com.ustadmobile.meshrabiya.vnet.wifi.HotspotType
7 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
8 | import org.junit.Assert
9 | import org.junit.Test
10 | import java.net.Inet6Address
11 |
12 | class MmcpOriginatorMessageTest {
13 |
14 | @Test
15 | fun givenOriginatorMessage_whenSerializedThenDeserialized_shouldBeEqual() {
16 | val sentTime = System.currentTimeMillis()
17 | val originatorMessage = MmcpOriginatorMessage(
18 | messageId = 1042,
19 | pingTimeSum = 200.toShort(),
20 | sentTime = sentTime,
21 | connectConfig = WifiConnectConfig(
22 | nodeVirtualAddr = 1000,
23 | ssid = "test",
24 | passphrase = "apassword",
25 | linkLocalAddr = Inet6Address.getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334").requireAsIpv6(),
26 | port = 1023,
27 | hotspotType = HotspotType.WIFIDIRECT_GROUP,
28 | persistenceType = HotspotPersistenceType.FULL,
29 | )
30 | )
31 |
32 | //Apply an offset to ensure this works as expected
33 | val originatorBytes = originatorMessage.toBytes()
34 | val byteArray = ByteArray(1500)
35 | val offset = 42
36 | System.arraycopy(originatorBytes, 0, byteArray, offset, originatorBytes.size)
37 |
38 | val messageFromBytes = MmcpOriginatorMessage.fromBytes(byteArray, offset)
39 |
40 | Assert.assertEquals(originatorMessage, messageFromBytes)
41 | }
42 |
43 | @Test
44 | fun givenOriginatorMessage_whenConvertedToPacketAndPingTimeIncremented_thenPingTimeShouldMatchExpectedVal() {
45 | val originatorMessage = MmcpOriginatorMessage(
46 | messageId = 1042,
47 | pingTimeSum = 32.toShort(),
48 | connectConfig = WifiConnectConfig(
49 | nodeVirtualAddr = 1000,
50 | ssid = "test",
51 | passphrase = "apassword",
52 | linkLocalAddr = Inet6Address.getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334").requireAsIpv6(),
53 | port = 1023,
54 | hotspotType = HotspotType.WIFIDIRECT_GROUP,
55 | persistenceType = HotspotPersistenceType.FULL,
56 | )
57 | )
58 |
59 | val packet = originatorMessage.toVirtualPacket(
60 | toAddr = ADDR_BROADCAST,
61 | fromAddr = 1000
62 | )
63 |
64 | val pingTimeIncrement = 32.toShort()
65 | MmcpOriginatorMessage.incrementPingTimeSum(packet, pingTimeIncrement)
66 |
67 | val messageFromPacket = MmcpMessage.fromVirtualPacket(packet) as MmcpOriginatorMessage
68 |
69 | Assert.assertEquals((originatorMessage.pingTimeSum + pingTimeIncrement).toShort(), messageFromPacket.pingTimeSum)
70 | Assert.assertEquals(originatorMessage.connectConfig, messageFromPacket.connectConfig)
71 | }
72 |
73 |
74 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'org.jetbrains.kotlin.plugin.serialization'
5 | id 'maven-publish'
6 | }
7 |
8 | android {
9 | namespace 'com.ustadmobile.httpoverbluetooth'
10 | compileSdk 33
11 |
12 | defaultConfig {
13 | minSdk 26
14 | targetSdk 33
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | consumerProguardFiles "consumer-rules.pro"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 |
27 | compileOptions {
28 | coreLibraryDesugaringEnabled true
29 |
30 | sourceCompatibility JavaVersion.VERSION_17
31 | targetCompatibility JavaVersion.VERSION_17
32 | }
33 |
34 | kotlinOptions {
35 | jvmTarget = JavaVersion.VERSION_17
36 | }
37 | }
38 |
39 | dependencies {
40 | implementation "androidx.core:core-ktx:$version_androidx_core"
41 | implementation "androidx.appcompat:appcompat:$version_appcompat"
42 | implementation "com.athaydes.rawhttp:rawhttp-core:$version_rawhttp"
43 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$version_kotlinx_serialization"
44 | implementation "androidx.datastore:datastore-preferences:$version_datastore"
45 |
46 |
47 | implementation "org.bouncycastle:bcprov-jdk18on:$version_bouncycastle"
48 | implementation "org.bouncycastle:bcpkix-jdk18on:$version_bouncycastle"
49 |
50 | implementation "com.github.seancfoley:ipaddress:$version_ip_address"
51 |
52 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$version_android_desugaring"
53 |
54 | testImplementation project(":test-shared")
55 | testImplementation "junit:junit:$version_junit"
56 | testImplementation "org.mockito.kotlin:mockito-kotlin:$version_kotlin_mockito"
57 | testImplementation "app.cash.turbine:turbine:$version_turbine"
58 |
59 | testImplementation "com.squareup.okhttp3:mockwebserver:$version_mockwebserver"
60 | testImplementation "com.squareup.okhttp3:okhttp:$version_okhttp"
61 |
62 | //As per: https://developer.android.com/topic/libraries/testing-support-library/packages.html#gradle-dependencies
63 | androidTestImplementation "androidx.test:runner:$version_android_junit_runner"
64 | androidTestImplementation "androidx.test:rules:$version_androidx_test_rules"
65 |
66 | androidTestImplementation project(":test-shared")
67 | androidTestImplementation "androidx.test.ext:junit:$version_android_test_ext_junit"
68 | androidTestImplementation "app.cash.turbine:turbine:$version_turbine"
69 | androidTestImplementation "org.mockito:mockito-android:$version_android_mockito"
70 | androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$version_kotlin_mockito"
71 | }
72 |
73 | publishing {
74 | publications {
75 | release(MavenPublication) {
76 | groupId = rootProject.group
77 | artifactId = project.name
78 | version = rootProject.version
79 |
80 | afterEvaluate {
81 | from components.release
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/test-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
34 |
36 |
38 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
57 |
58 |
63 |
64 |
67 |
68 |
69 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
87 |
88 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/viewmodel/SelectDestNodeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.viewmodel
2 |
3 | import android.net.Uri
4 | import android.util.Log
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.ustadmobile.meshrabiya.ext.addressToByteArray
8 | import com.ustadmobile.meshrabiya.ext.addressToDotNotation
9 | import com.ustadmobile.meshrabiya.log.MNetLogger
10 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
11 | import com.ustadmobile.meshrabiya.testapp.appstate.FabState
12 | import com.ustadmobile.meshrabiya.testapp.server.TestAppServer
13 | import com.ustadmobile.meshrabiya.vnet.AndroidVirtualNode
14 | import com.ustadmobile.meshrabiya.vnet.VirtualNode
15 | import kotlinx.coroutines.Dispatchers
16 | import kotlinx.coroutines.flow.Flow
17 | import kotlinx.coroutines.flow.MutableStateFlow
18 | import kotlinx.coroutines.flow.asStateFlow
19 | import kotlinx.coroutines.flow.update
20 | import kotlinx.coroutines.launch
21 | import kotlinx.coroutines.withContext
22 | import org.kodein.di.DI
23 | import org.kodein.di.instance
24 | import java.net.InetAddress
25 |
26 | data class SelectDestNodeUiState(
27 | val nodes: Map = emptyMap(),
28 | val contactingInProgressDevice: String? = null,
29 | val error: String? = null,
30 | val sendingUri: String = "",
31 | val appUiState: AppUiState = AppUiState(),
32 | )
33 |
34 | class SelectDestNodeViewModel(
35 | di: DI,
36 | private val uriToSend: String,
37 | private val navigateOnDone: () -> Unit,
38 | ): ViewModel() {
39 |
40 | private val _uiState = MutableStateFlow(SelectDestNodeUiState())
41 |
42 | val uiState: Flow = _uiState.asStateFlow()
43 |
44 | private val testAppServer: TestAppServer by di.instance()
45 |
46 | private val virtualNode: AndroidVirtualNode by di.instance()
47 |
48 | private val logger: MNetLogger by di.instance()
49 |
50 | init {
51 | _uiState.update { prev ->
52 | prev.copy(
53 | sendingUri = uriToSend,
54 | appUiState = AppUiState(
55 | title = "Select receiver",
56 | fabState = FabState(visible = false),
57 | )
58 | )
59 | }
60 |
61 | viewModelScope.launch {
62 | virtualNode.state.collect {
63 | _uiState.update { prev ->
64 | prev.copy(
65 | nodes = it.originatorMessages
66 | )
67 | }
68 | }
69 | }
70 | }
71 |
72 | fun onClickDest(
73 | destNodeAddr: Int,
74 | ) {
75 | val destInetAddr = InetAddress.getByAddress(destNodeAddr.addressToByteArray())
76 | _uiState.update { prev ->
77 | prev.copy(
78 | contactingInProgressDevice = destNodeAddr.addressToDotNotation()
79 | )
80 | }
81 |
82 | viewModelScope.launch {
83 | val transfer = withContext(Dispatchers.IO) {
84 | try {
85 | testAppServer.addOutgoingTransfer(
86 | uri = Uri.parse(uriToSend),
87 | toNode = destInetAddr,
88 | )
89 | }catch(e: Exception) {
90 | logger(Log.ERROR, "Exception attempting to send to destination", e)
91 | _uiState.update { prev ->
92 | prev.copy(
93 | error = e.toString()
94 | )
95 | }
96 | null
97 | }
98 | }
99 |
100 | if(transfer != null)
101 | navigateOnDone()
102 | }
103 | }
104 |
105 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/screens/SelectDestNodeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.screens
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.material3.CircularProgressIndicator
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.collectAsState
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
18 | import androidx.compose.ui.unit.dp
19 | import androidx.lifecycle.viewmodel.compose.viewModel
20 | import com.ustadmobile.meshrabiya.testapp.ViewModelFactory
21 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
22 | import com.ustadmobile.meshrabiya.testapp.viewmodel.SelectDestNodeUiState
23 | import com.ustadmobile.meshrabiya.testapp.viewmodel.SelectDestNodeViewModel
24 | import org.kodein.di.compose.localDI
25 |
26 | @Composable
27 | fun SelectDestNodeScreen(
28 | uriToSend: String,
29 | onSetAppUiState: (AppUiState) -> Unit,
30 | navigateOnDone: () -> Unit,
31 | viewModel: SelectDestNodeViewModel = viewModel(
32 | factory = ViewModelFactory(
33 | di = localDI(),
34 | owner = LocalSavedStateRegistryOwner.current,
35 | vmFactory = {
36 | SelectDestNodeViewModel(it, uriToSend, navigateOnDone)
37 | },
38 | defaultArgs = null,
39 | )
40 | )
41 | ){
42 | val uiState by viewModel.uiState.collectAsState(SelectDestNodeUiState())
43 | LaunchedEffect(uiState.appUiState) {
44 | onSetAppUiState(uiState.appUiState)
45 | }
46 | SelectDestNodeScreen(
47 | uiState = uiState,
48 | onClickNode = viewModel::onClickDest,
49 | )
50 | }
51 |
52 | @Composable
53 | fun SelectDestNodeScreen(
54 | uiState: SelectDestNodeUiState,
55 | onClickNode: (Int) -> Unit,
56 | ) {
57 | val inProgressDevice = uiState.contactingInProgressDevice
58 |
59 | LazyColumn(
60 | modifier = Modifier.fillMaxSize()
61 | ) {
62 | uiState.error?.also { error ->
63 | item(key = "error") {
64 | Text("ERROR: $error")
65 | }
66 | }
67 |
68 | if(inProgressDevice != null) {
69 | item("inprogress") {
70 | Column(
71 | modifier = Modifier.fillMaxWidth(),
72 | horizontalAlignment = Alignment.CenterHorizontally,
73 | ) {
74 | CircularProgressIndicator(
75 | modifier = Modifier.padding(vertical = 8.dp),
76 | )
77 | Text(
78 | modifier = Modifier.padding(vertical = 8.dp),
79 | text = "Contacting $inProgressDevice\nThis might take a few seconds.",
80 | )
81 | }
82 | }
83 | }else {
84 | items(
85 | items = uiState.nodes.entries.toList(),
86 | key = { it.key }
87 | ){ nodeEntry ->
88 | NodeListItem(
89 | nodeAddr = nodeEntry.key,
90 | nodeEntry = nodeEntry.value,
91 | onClick = {
92 | onClickNode(nodeEntry.key)
93 | }
94 | )
95 | }
96 | }
97 |
98 |
99 | }
100 |
101 | }
102 |
103 |
104 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/VirtualNodeDatagramSocket.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import android.net.Network
4 | import android.util.Log
5 | import com.ustadmobile.meshrabiya.log.MNetLogger
6 | import com.ustadmobile.meshrabiya.ext.addressToDotNotation
7 | import java.io.Closeable
8 | import java.net.DatagramPacket
9 | import java.net.DatagramSocket
10 | import java.net.InetAddress
11 | import java.util.concurrent.ExecutorService
12 | import java.util.concurrent.Future
13 |
14 | /**
15 | *
16 | * VirtualNodeDatagramSocket listens on the real network interface. It uses the executor service
17 | * to run a thread that will receive all packets, convert them from a DatagramPacket into a
18 | * VirtualPacket, and then give them to the VirtualRouter.
19 | *
20 | * @param socket - the underlying DatagramSocket to use - this can be bound to a network, interface etc if required
21 | * neighbor connects.
22 | * @param boundNetwork The Network object that the DatagramSocket is/will be bound to, if any. This
23 | * is needed if/when we want to establish a TCP connection. Because the
24 | * VirtualNodeDatagramSocket reference is kept as part of the originator message,
25 | * and it is created at the time the network object is available, this is the
26 | * most convenient and logical place to keep this reference.
27 | */
28 | class VirtualNodeDatagramSocket(
29 | private val socket: DatagramSocket,
30 | private val localNodeVirtualAddress: Int,
31 | ioExecutorService: ExecutorService,
32 | private val router: VirtualRouter,
33 | private val logger: MNetLogger,
34 | name: String? = null,
35 | val boundNetwork: Network? = null,
36 | ): Runnable, Closeable {
37 |
38 | private val future: Future<*>
39 |
40 | private val logPrefix: String
41 |
42 | val localPort: Int = socket.localPort
43 |
44 | init {
45 | logPrefix = buildString {
46 | append("[VirtualNodeDatagramSocket for ${localNodeVirtualAddress.addressToDotNotation()} ")
47 | if(name != null)
48 | append("- $name")
49 | append("] ")
50 | }
51 | future = ioExecutorService.submit(this)
52 | }
53 |
54 | override fun run() {
55 | val buffer = ByteArray(VirtualPacket.MAX_PAYLOAD_SIZE)
56 | logger(Log.DEBUG, "$logPrefix Started on ${socket.localPort} waiting for first packet", null)
57 |
58 | while(!Thread.interrupted() && !socket.isClosed) {
59 | try {
60 | val rxPacket = DatagramPacket(buffer, 0, buffer.size)
61 | socket.receive(rxPacket)
62 |
63 | val rxVirtualPacket = VirtualPacket.fromDatagramPacket(rxPacket)
64 | router.route(
65 | packet = rxVirtualPacket,
66 | datagramPacket = rxPacket,
67 | virtualNodeDatagramSocket = this,
68 | )
69 | }catch(e: Exception) {
70 | if(!socket.isClosed)
71 | logger(Log.WARN, "$logPrefix : run : exception handling packet", e)
72 | }
73 | }
74 | logger(Log.DEBUG, "$logPrefix : run : finished")
75 | }
76 |
77 |
78 | /**
79 | *
80 | */
81 | fun send(
82 | nextHopAddress: InetAddress,
83 | nextHopPort: Int,
84 | virtualPacket: VirtualPacket
85 | ) {
86 | val datagramPacket = virtualPacket.toDatagramPacket()
87 | datagramPacket.address = nextHopAddress
88 | datagramPacket.port = nextHopPort
89 | socket.send(datagramPacket)
90 | }
91 |
92 | fun close(closeSocket: Boolean) {
93 | future.cancel(true)
94 | socket.takeIf { closeSocket }?.close()
95 | }
96 |
97 | override fun close() {
98 | close(false)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/MeshrabiyaConnectLink.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import com.ustadmobile.meshrabiya.ext.addressToDotNotation
4 | import com.ustadmobile.meshrabiya.vnet.bluetooth.MeshrabiyaBluetoothState
5 | import com.ustadmobile.meshrabiya.vnet.wifi.WifiConnectConfig
6 | import kotlinx.serialization.json.Json
7 | import java.net.InetAddress
8 | import java.net.URLDecoder
9 | import com.ustadmobile.meshrabiya.ext.requireAddressAsInt
10 | import java.net.URLEncoder
11 |
12 | /**
13 | *
14 | */
15 | data class MeshrabiyaConnectLink(
16 | val uri: String,
17 | val virtualAddress: Int,
18 | val hotspotConfig: WifiConnectConfig?,
19 | val bluetoothConfig: MeshrabiyaBluetoothState?
20 | ) {
21 |
22 | companion object {
23 |
24 | const val PROTO = "meshrabiya"
25 |
26 | private const val PROTO_PREFIX = "${PROTO}://"
27 |
28 | fun fromComponents(
29 | nodeAddr: Int,
30 | port: Int,
31 | hotspotConfig: WifiConnectConfig?,
32 | bluetoothConfig: MeshrabiyaBluetoothState?,
33 | json: Json,
34 | ) : MeshrabiyaConnectLink {
35 | val uri = buildString {
36 | append("$PROTO_PREFIX${nodeAddr.addressToDotNotation()}:$port/?")
37 | if(hotspotConfig != null) {
38 | append("hotspot=")
39 | append(
40 | URLEncoder.encode(json.encodeToString(
41 | WifiConnectConfig.serializer(), hotspotConfig
42 | ), "UTF-8")
43 | )
44 | }
45 | if(hotspotConfig != null && bluetoothConfig != null) {
46 | append("&")
47 | }
48 | if(bluetoothConfig != null) {
49 | append("bluetooth=")
50 | append(
51 | URLEncoder.encode(json.encodeToString(
52 | MeshrabiyaBluetoothState.serializer(), bluetoothConfig
53 | ), "UTF-8")
54 | )
55 | }
56 | }
57 |
58 | return MeshrabiyaConnectLink(
59 | uri = uri,
60 | virtualAddress = nodeAddr,
61 | hotspotConfig = hotspotConfig,
62 | bluetoothConfig = bluetoothConfig,
63 | )
64 | }
65 |
66 | fun parseUri(
67 | uri: String,
68 | json: Json = Json,
69 | ): MeshrabiyaConnectLink {
70 | val uriLowerCase = uri.lowercase()
71 | if(!uriLowerCase.startsWith(PROTO_PREFIX))
72 | throw IllegalArgumentException("Meshrabiya connect url must start with $PROTO://")
73 |
74 | val addr = uri.substringAfter(PROTO_PREFIX).substringBefore(":")
75 | val inetAddr = InetAddress.getByName(addr)
76 |
77 | val searchStr = uri.substringAfter("?")
78 | val searchComponents = searchStr.split('&').map { param ->
79 | param.split("=", limit = 2).let {
80 | Pair(URLDecoder.decode(it[0], "UTF-8"), URLDecoder.decode(it[1], "UTF-8"))
81 | }
82 | }.toMap()
83 | val hotspotConfig = searchComponents["hotspot"]?.let {
84 | json.decodeFromString(WifiConnectConfig.serializer(), it)
85 | }
86 |
87 | val bluetoothConfig = searchComponents["bluetooth"]?.let {
88 | json.decodeFromString(MeshrabiyaBluetoothState.serializer(), it)
89 | }
90 |
91 | return MeshrabiyaConnectLink(
92 | uri = uri,
93 | virtualAddress = inetAddr.requireAddressAsInt(),
94 | hotspotConfig = hotspotConfig,
95 | bluetoothConfig = bluetoothConfig,
96 | )
97 | }
98 |
99 | }
100 |
101 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/VirtualPacketHeader.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 |
6 | /**
7 | * Virtual packet structure
8 | * Header:
9 | * @param toAddr - the Virtual Node address this packet wants to reach. 0 = send over local link only.
10 | * @param port the Virtual Port on the destination Vitual Noe that this packet wants to reach
11 | * @param fromAddr the Virtual Node that originally sent this packet
12 | * @param fromPort the virtual port that this packet was sent from
13 | * @param lastHopAddr the virtual node address of the most recent hop. E.g. where the packet is
14 | * sent from node A to node B, then node B to node C, the lastHop would be A when the packet
15 | * is sent from A to B, and then B when the packet is sent from B to C.
16 | * @param hopCount the total number of hops this packet has taken. Starts at 1 when first sent and
17 | * is incremented on each hop.
18 | * @param maxHops the maximum number of hops that this packet should live for. If exceeded, packet is
19 | * dropped
20 | * @param payloadSize the size of the payload data
21 | *
22 | * Packet size/structure:
23 | * To address (32bit int)
24 | * to port (16bit short)
25 | * from address (32bit int)
26 | * from port (16bit short)
27 | * lastHopAddr (32 bit int)
28 | * hopCount (8bit byte)
29 | * maxHops (8bit byte)
30 | * payloadSize (16bit short)
31 | * payload (byte array where size = payloadSize)
32 | */
33 |
34 | data class VirtualPacketHeader(
35 | val toAddr: Int,
36 | val toPort: Int,
37 | val fromAddr: Int,
38 | val fromPort: Int,
39 | val lastHopAddr: Int,
40 | val hopCount: Byte,
41 | val maxHops: Byte,
42 | val payloadSize: Int, //Max size should be in line with MTU e.g. 1500. Stored as short
43 | ) {
44 |
45 | init {
46 | if(payloadSize > MAX_PAYLOAD)
47 | throw IllegalArgumentException("Payload size must not be > $MAX_PAYLOAD")
48 | }
49 |
50 | fun toBytes(
51 | byteArray: ByteArray,
52 | offset: Int
53 | ) {
54 | val buf = ByteBuffer.wrap(byteArray, offset, HEADER_SIZE).order(ByteOrder.BIG_ENDIAN)
55 | buf.putInt(toAddr)
56 | buf.putShort(toPort.toShort())
57 | buf.putInt(fromAddr)
58 | buf.putShort(fromPort.toShort())
59 | buf.putInt(lastHopAddr)
60 | buf.put(hopCount)
61 | buf.put(maxHops)
62 | buf.putShort(payloadSize.toShort())
63 | }
64 |
65 | fun toBytes(): ByteArray {
66 | return ByteArray(HEADER_SIZE).also {
67 | toBytes(it, 0)
68 | }
69 | }
70 |
71 | companion object {
72 |
73 | @Suppress("LocalVariableName", "UsePropertyAccessSyntax")
74 | fun fromBytes(
75 | bytes: ByteArray,
76 | offset: Int = 0,
77 | ): VirtualPacketHeader {
78 | val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
79 | buf.position(offset)
80 | val _toAddr = buf.getInt()
81 | val _toPort = buf.getShort()
82 | val _fromAddr = buf.getInt()
83 | val _fromPort = buf.getShort()
84 | val _lastHopAddr = buf.getInt()
85 | val _hopCount = buf.get()
86 | val _maxHops = buf.get()
87 | val _payloadSize = buf.getShort()
88 |
89 | return VirtualPacketHeader(
90 | toAddr = _toAddr,
91 | toPort = _toPort.toInt(),
92 | fromAddr = _fromAddr,
93 | fromPort = _fromPort.toInt(),
94 | lastHopAddr = _lastHopAddr,
95 | hopCount = _hopCount,
96 | maxHops = _maxHops,
97 | payloadSize = _payloadSize.toInt(),
98 | )
99 | }
100 |
101 | //Size of all header fields in bytes (as above)
102 | const val HEADER_SIZE = 20
103 |
104 | const val MAX_PAYLOAD = 2000
105 |
106 |
107 | }
108 | }
--------------------------------------------------------------------------------
/lib-meshrabiya/src/main/java/com/ustadmobile/meshrabiya/vnet/socket/ChainSocketFactoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.vnet.socket
2 |
3 | import android.util.Log
4 | import com.ustadmobile.meshrabiya.ext.prefixMatches
5 | import com.ustadmobile.meshrabiya.log.MNetLogger
6 | import com.ustadmobile.meshrabiya.vnet.VirtualRouter
7 | import java.net.InetAddress
8 | import java.net.Socket
9 | import javax.net.SocketFactory
10 |
11 | class ChainSocketFactoryImpl(
12 | internal val virtualRouter: VirtualRouter,
13 | private val systemSocketFactory: SocketFactory = getDefault(),
14 | private val logger: MNetLogger,
15 | ) : ChainSocketFactory() {
16 |
17 | private val logPrefix: String = "[ChainSocketFactoryImpl for ${virtualRouter.address}]"
18 |
19 | private fun createSocketForVirtualAddress(
20 | address: InetAddress,
21 | port: Int,
22 | localAddress: InetAddress? = null,
23 | localPort: Int? = null
24 | ) : ChainSocketResult {
25 | try {
26 | val nextHop = virtualRouter.lookupNextHopForChainSocket(address, port)
27 | val socketFactory = nextHop.network?.socketFactory ?: systemSocketFactory
28 | val socket = if(localAddress != null && localPort != null) {
29 | socketFactory.createSocket(nextHop.address, nextHop.port, localAddress, localPort)
30 | }else {
31 | socketFactory.createSocket(nextHop.address, nextHop.port)
32 | }
33 |
34 | socket.initializeChainIfNotFinalDest(
35 | ChainSocketInitRequest(
36 | virtualDestAddr = address,
37 | virtualDestPort = port,
38 | fromAddr = virtualRouter.address
39 | ),
40 | nextHop
41 | )
42 |
43 | logger(Log.INFO, "$logPrefix created socket to $address:$port " +
44 | "nexthop = ${nextHop.address}:${nextHop.port}")
45 | return ChainSocketResult(socket, nextHop)
46 | }catch(e: Exception) {
47 | logger(Log.ERROR, "$logPrefix exception creating socket", e)
48 | throw e
49 | }
50 | }
51 |
52 | private fun InetAddress.isVirtualAddress(): Boolean {
53 | return prefixMatches(virtualRouter.networkPrefixLength, virtualRouter.address)
54 | }
55 |
56 | override fun createSocket(host: String, port: Int): Socket {
57 | val address = InetAddress.getByName(host)
58 | return if(address.isVirtualAddress()) {
59 | createSocketForVirtualAddress(address, port).socket
60 | }else {
61 | systemSocketFactory.createSocket(host, port)
62 | }
63 | }
64 |
65 | override fun createSocket(host: String, port: Int, localAddress: InetAddress, localPort: Int): Socket {
66 | val address = InetAddress.getByName(host)
67 | return if(address.isVirtualAddress()) {
68 | createSocketForVirtualAddress(address, port, localAddress, localPort).socket
69 | }else {
70 | systemSocketFactory.createSocket(host, port, localAddress, localPort)
71 | }
72 | }
73 |
74 | override fun createSocket(address: InetAddress, port: Int): Socket {
75 | return if(address.isVirtualAddress()) {
76 | createSocketForVirtualAddress(address, port).socket
77 | }else {
78 | systemSocketFactory.createSocket(address, port)
79 | }
80 | }
81 |
82 | override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
83 | return if(address.isVirtualAddress()) {
84 | createSocketForVirtualAddress(address, port, localAddress, localPort).socket
85 | }else {
86 | systemSocketFactory.createSocket(address, port, localAddress, localPort)
87 | }
88 | }
89 |
90 | override fun createSocket(): Socket {
91 | return ChainSocket(virtualRouter, logger)
92 | }
93 |
94 | override fun createChainSocket(address: InetAddress, port: Int): ChainSocketResult {
95 | return createSocketForVirtualAddress(address, port)
96 | }
97 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/screens/NeighborNodeListScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp.screens
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.FilterChip
10 | import androidx.compose.material3.ListItem
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.LaunchedEffect
14 | import androidx.compose.runtime.collectAsState
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
18 | import androidx.compose.ui.unit.dp
19 | import androidx.lifecycle.viewmodel.compose.viewModel
20 | import com.ustadmobile.meshrabiya.ext.addressToDotNotation
21 | import com.ustadmobile.meshrabiya.testapp.ViewModelFactory
22 | import com.ustadmobile.meshrabiya.testapp.appstate.AppUiState
23 | import com.ustadmobile.meshrabiya.testapp.viewmodel.NeighborNodeListUiState
24 | import com.ustadmobile.meshrabiya.testapp.viewmodel.NeighborNodeListViewModel
25 | import com.ustadmobile.meshrabiya.vnet.VirtualNode
26 | import org.kodein.di.compose.localDI
27 |
28 | @Composable
29 | fun NeighborNodeListScreen(
30 | viewModel: NeighborNodeListViewModel = viewModel(
31 | factory = ViewModelFactory(
32 | di = localDI(),
33 | owner = LocalSavedStateRegistryOwner.current,
34 | vmFactory = {
35 | NeighborNodeListViewModel(it)
36 | },
37 | defaultArgs = null,
38 | )
39 | ),
40 | onSetAppUiState: (AppUiState) -> Unit,
41 | ) {
42 | val uiState by viewModel.uiState.collectAsState(NeighborNodeListUiState())
43 |
44 | LaunchedEffect(uiState.appUiState) {
45 | onSetAppUiState(uiState.appUiState)
46 | }
47 |
48 |
49 | NeighborNodeListScreen(
50 | uiState = uiState,
51 | onClickFilter = viewModel::onClickFilterChip
52 | )
53 | }
54 |
55 | @OptIn(ExperimentalMaterial3Api::class)
56 | @Composable
57 | fun NeighborNodeListScreen(
58 | uiState: NeighborNodeListUiState,
59 | onClickFilter: (NeighborNodeListUiState.Companion.Filter) -> Unit = { },
60 | ) {
61 | LazyColumn {
62 | item(key = "filterchips") {
63 | Row(modifier = Modifier.padding(horizontal = 8.dp)){
64 | NeighborNodeListUiState.Companion.Filter.values().forEach { filter ->
65 | FilterChip(
66 | modifier = Modifier.padding(8.dp),
67 | selected = uiState.filter == filter,
68 | onClick = {
69 | onClickFilter(filter)
70 | },
71 | label = {
72 | Text(filter.label)
73 | }
74 | )
75 | }
76 | }
77 | }
78 |
79 |
80 |
81 | items(
82 | items = uiState.nodes.entries.toList() ,
83 | key = { it.key }
84 | ) { nodeEntry ->
85 | NodeListItem(nodeEntry.key, nodeEntry.value)
86 | }
87 |
88 | }
89 |
90 | }
91 |
92 | @Composable
93 | fun NodeListItem(
94 | nodeAddr: Int,
95 | nodeEntry: VirtualNode.LastOriginatorMessage,
96 | onClick: (() -> Unit)? = null,
97 | ) {
98 | ListItem(
99 | modifier = Modifier.let {
100 | if(onClick != null) {
101 | it.clickable(
102 | onClick = onClick
103 | )
104 | }else {
105 | it
106 | }
107 | },
108 | headlineContent = {
109 | Text(nodeAddr.addressToDotNotation())
110 | },
111 | supportingContent = {
112 | Text("Ping ${nodeEntry.originatorMessage.pingTimeSum}ms " +
113 | " Hops: ${nodeEntry.hopCount} ")
114 | },
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/ustadmobile/meshrabiya/testapp/MNetLoggerAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.ustadmobile.meshrabiya.testapp
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import com.ustadmobile.meshrabiya.MeshrabiyaConstants
6 | import com.ustadmobile.meshrabiya.log.MNetLogger
7 | import com.ustadmobile.meshrabiya.ext.trimIfExceeds
8 | import com.ustadmobile.meshrabiya.log.LogLine
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.Job
12 | import kotlinx.coroutines.channels.Channel
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.flow.asStateFlow
16 | import kotlinx.coroutines.flow.update
17 | import kotlinx.coroutines.launch
18 | import java.io.File
19 | import java.text.DateFormat
20 | import java.util.Date
21 |
22 | class MNetLoggerAndroid(
23 | private val deviceInfoStr: String,
24 | private val minLogLevel: Int = Log.VERBOSE,
25 | private val logHistoryLines: Int = 300,
26 | private val logFile: File? = null,
27 | ): MNetLogger() {
28 |
29 | val epochTime = System.currentTimeMillis()
30 |
31 | private val _recentLogs = MutableStateFlow(emptyList())
32 |
33 | val recentLogs: Flow> = _recentLogs.asStateFlow()
34 |
35 | private val logScope = CoroutineScope(Dispatchers.IO + Job())
36 |
37 | private val logChannel = Channel(Channel.UNLIMITED)
38 |
39 | init {
40 | logScope.launch {
41 | logFile?.parentFile?.takeIf { !it.exists() }?.mkdirs()
42 | val startTime = DateFormat.getTimeInstance().format(Date())
43 | logFile?.appendText("Meshrabiya Session start: $startTime\n$deviceInfoStr\n")
44 |
45 | for(logLine in logChannel) {
46 | val time = (System.currentTimeMillis() - epochTime) / 1000.toFloat()
47 | val rounded = (time * 100).toInt() / 100.toFloat()
48 | logFile?.appendText("${priorityLabel(logLine.priority)}: t+${rounded}s : ${logLine.line}\n")
49 | }
50 | }
51 | }
52 |
53 | private fun doLog(priority: Int, message: String, exception: Exception?) {
54 | when (priority) {
55 | Log.VERBOSE -> Log.v(MeshrabiyaConstants.LOG_TAG, message, exception)
56 | Log.DEBUG -> Log.d(MeshrabiyaConstants.LOG_TAG, message, exception)
57 | Log.INFO -> Log.i(MeshrabiyaConstants.LOG_TAG, message, exception)
58 | Log.WARN -> Log.w(MeshrabiyaConstants.LOG_TAG, message, exception)
59 | Log.ERROR -> Log.e(MeshrabiyaConstants.LOG_TAG, message, exception)
60 | Log.ASSERT -> Log.wtf(MeshrabiyaConstants.LOG_TAG, message, exception)
61 | }
62 |
63 | val logDisplay = buildString {
64 | append(message)
65 | if (exception != null) {
66 | append(" Exception: ")
67 | append(exception.toString())
68 | }
69 | }
70 |
71 | val logLine = LogLine(logDisplay, priority, System.currentTimeMillis())
72 | _recentLogs.update { prev ->
73 | buildList {
74 | add(logLine)
75 | addAll(prev.trimIfExceeds(logHistoryLines - 1))
76 | }
77 | }
78 | logChannel.takeIf { logFile != null }?.trySend(logLine)
79 | }
80 |
81 | override fun invoke(priority: Int, message: () -> String, exception: Exception?) {
82 | if(priority >= minLogLevel)
83 | doLog(priority, message(), exception)
84 | }
85 |
86 | override fun invoke(priority: Int, message: String, exception: Exception?) {
87 | if(priority >= minLogLevel)
88 | doLog(priority, message, exception)
89 | }
90 |
91 | /**
92 | * Export logs with time/date stamp, device info, etc.
93 | */
94 | fun exportAsString(context: Context): String {
95 | return buildString {
96 | append(context.meshrabiyaDeviceInfoStr())
97 | append("==Logs==\n")
98 |
99 | _recentLogs.value.reversed().forEach {
100 | append(it.toString(epochTime))
101 | append("\n")
102 | }
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------