[WIP] added a packet system with the network server and client

This commit is contained in:
Faraphel 2024-05-02 12:46:50 +02:00
parent 653d10260f
commit ddb63a47ed
21 changed files with 438 additions and 150 deletions

View file

@ -5,26 +5,26 @@
<entry key="app"> <entry key="app">
<State> <State>
<multipleDevicesSelectedInDropDown value="true" /> <multipleDevicesSelectedInDropDown value="true" />
<targetsSelectedWithDialog> <runningDeviceTargetsSelectedWithDialog>
<Target> <Target>
<type value="QUICK_BOOT_TARGET" /> <type value="RUNNING_DEVICE_TARGET" />
<deviceKey> <deviceKey>
<Key> <Key>
<type value="VIRTUAL_DEVICE_PATH" /> <type value="SERIAL_NUMBER" />
<value value="C:\Users\RC606\.android\avd\Small_Phone_API_26.avd" /> <value value="ypee7lnbpv9x7lvs" />
</Key> </Key>
</deviceKey> </deviceKey>
</Target> </Target>
<Target> <Target>
<type value="QUICK_BOOT_TARGET" /> <type value="RUNNING_DEVICE_TARGET" />
<deviceKey> <deviceKey>
<Key> <Key>
<type value="VIRTUAL_DEVICE_PATH" /> <type value="SERIAL_NUMBER" />
<value value="C:\Users\RC606\.android\avd\Small_Phone_API_26_-_2.avd" /> <value value="2XJDU17923000406" />
</Key> </Key>
</deviceKey> </deviceKey>
</Target> </Target>
</targetsSelectedWithDialog> </runningDeviceTargetsSelectedWithDialog>
</State> </State>
</entry> </entry>
</value> </value>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" /> <option name="version" value="1.9.23" />
</component> </component>
</project> </project>

View file

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="Kotlin SDK" project-jdk-type="KotlinSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

124
.idea/uiDesigner.xml Normal file
View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

View file

@ -1,7 +1,8 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
kotlin("android")
kotlin("plugin.serialization")
} }
android { android {
@ -41,7 +42,7 @@ android {
compose = true compose = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.5.1" kotlinCompilerExtensionVersion = "1.5.13"
} }
packaging { packaging {
resources { resources {
@ -51,15 +52,16 @@ android {
} }
dependencies { dependencies {
implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-ktx:1.13.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2") implementation("androidx.activity:activity-compose:1.9.0")
implementation(platform("androidx.compose:compose-bom:2023.08.00")) implementation(platform("androidx.compose:compose-bom:2023.08.00"))
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3")
implementation("androidx.room:room-ktx:2.6.1") implementation("androidx.room:room-ktx:2.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View file

@ -11,6 +11,7 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.faraphel.tasks_valider.connectivity.packets.PacketPing
import com.faraphel.tasks_valider.connectivity.wifi_p2p.WifiP2pHelper import com.faraphel.tasks_valider.connectivity.wifi_p2p.WifiP2pHelper
import com.faraphel.tasks_valider.database.Database import com.faraphel.tasks_valider.database.Database
import com.faraphel.tasks_valider.ui.screen.room.RoomScreen import com.faraphel.tasks_valider.ui.screen.room.RoomScreen
@ -75,4 +76,9 @@ class MainActivity : ComponentActivity() {
// disable the WiFi-Direct events // disable the WiFi-Direct events
this.unregisterReceiver(this.p2pHelper) this.unregisterReceiver(this.p2pHelper)
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// TODO(Faraphel): save the current state to reload it later
}
} }

View file

@ -1,49 +0,0 @@
package com.faraphel.tasks_valider.connectivity
import android.util.Log
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
class DemoClient(hostname: InetAddress, port: Int) {
companion object {
var SOCKET_TIMEOUT = 120 * 1000 ///< TEST(Faraphel): long timeout for testing
}
private var server = Socket()
private var address = InetSocketAddress(hostname, port)
init {
server.soTimeout = SOCKET_TIMEOUT
}
/**
* Try to contact the server once
*/
fun once() {
Log.d("demo-client", "connecting to the server...")
this.server.connect(this.address, SOCKET_TIMEOUT)
Log.d("demo-client", "connected !")
Log.d("demo-client", "sending message...")
server.getOutputStream().write(42) // send a byte
Log.d("demo-client", "message sent !")
}
/**
* Try to contact the server indefinitely
*/
fun start() {
Thread {
while (true) {
try {
this.once()
} catch (exception: Exception) {
Log.w("demo-client", "an error occured: $exception")
}
Thread.sleep(1000)
}
}.start()
}
}

View file

@ -1,50 +0,0 @@
package com.faraphel.tasks_valider.connectivity
import android.util.Log
import java.net.ServerSocket
class DemoServer(port: Int) {
companion object {
var SOCKET_TIMEOUT = 120 * 1000 ///< TEST(Faraphel): long timeout for testing
}
private var server = ServerSocket(port)
init {
server.soTimeout = SOCKET_TIMEOUT
}
/**
* Accept and treat a client once
*/
private fun once() {
// wait for a client connexion
Log.d("demo-server", "waiting for a client...")
val client = server.accept()
Log.d("demo-server", "client connected !")
// once connected, wait and read a byte from the client.
val value = client.getInputStream().read()
Log.d("demo-server", "client sent data : $value")
// TODO(Faraphel): an abstraction layer should be added in the future to receive type of packets and read the data
}
/**
* Accept and threat clients indefinitely
*/
fun start() {
Thread {
while (true) {
try {
this.once()
} catch (exception: Exception) {
Log.w("demo-server", "an error occured: $exception")
}
Thread.sleep(1000)
}
}.start()
}
}

View file

@ -0,0 +1,29 @@
package com.faraphel.tasks_valider.connectivity.packets
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/**
* Base for a packet that can be encoded and decoded for a Socket
*/
@Serializable
sealed class BasePacket {
companion object {
/**
* Create a new instance from an array of bytes.
* @param data: data obtained from the toBytes function.
*/
inline fun <reified Packet: BasePacket> fromBytes(data: ByteArray): Packet {
return Json.decodeFromString<Packet>(data.toString())
}
}
/**
* Encode the content of the packet into an array of bytes
*/
fun toBytes(): ByteArray {
return Json.encodeToString(this).encodeToByteArray()
}
}

View file

@ -0,0 +1,10 @@
package com.faraphel.tasks_valider.connectivity.packets
import kotlinx.serialization.Serializable
/**
* This is a simple packet class to test a connection
*/
@Serializable
data object PacketPing : BasePacket()

View file

@ -0,0 +1,41 @@
package com.faraphel.tasks_valider.connectivity.room
import android.util.Log
import com.faraphel.tasks_valider.connectivity.packets.BasePacket
import com.faraphel.tasks_valider.connectivity.packets.PacketPing
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
/**
* A client to handle the room connection.
* @param address the address of the server
* @param port the port of the server
*/
class RoomClient(
private val address: InetAddress,
private val port: Int
) {
private val server = Socket()
fun start() {
Log.d("room-client", "connecting to the server...")
try {
server.connect(InetSocketAddress(address, port), 10_000)
} catch (exception: Exception) {
Log.e("room-client", "could not connect to the server", exception)
return
}
Log.d("room-client", "connection successful !")
val serverIn = server.getInputStream()
val serverOut = server.getOutputStream()
serverOut.write(PacketPing.toBytes())
val data = serverIn.readBytes()
val packet = BasePacket.fromBytes<PacketPing>(data)
Log.d("room-client", packet.toString())
}
}

View file

@ -0,0 +1,50 @@
package com.faraphel.tasks_valider.connectivity.room
import android.util.Log
import com.faraphel.tasks_valider.connectivity.packets.PacketPing
import java.net.ServerSocket
import java.net.Socket
/**
* A server to handle the room connection.
* @param port the port of the server
* @param timeout the timeout for a client (in milliseconds)
*/
class RoomServer(
private val port: Int,
private val timeout: Int = 10_000
) {
private var server = ServerSocket(port)
init {
server.soTimeout = 0 // accepting clients take an infinite timeout
}
/**
* Accept and treat a client
*/
private fun handleClient(client: Socket) {
// TODO(Faraphel): should every client be handled in a new small thread ?
// Create the thread here and handle it until the connection is broken
val clientIn = client.getInputStream()
val clientOut = client.getOutputStream()
Log.i("room-server", "data: ${PacketPing.toBytes().toList()}")
clientOut.write(PacketPing.toBytes())
}
/**
* Accept connections and treat them
*/
fun start() {
Thread {
while (true) {
val client = server.accept()
client.soTimeout = timeout // set the timeout for the communication
this.handleClient(client)
}
}.start()
}
}

View file

@ -1,13 +1,19 @@
package com.faraphel.tasks_valider.ui.screen.room package com.faraphel.tasks_valider.ui.screen.room
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList import android.net.wifi.p2p.WifiP2pDeviceList
import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager import android.net.wifi.p2p.WifiP2pManager
import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.faraphel.tasks_valider.connectivity.room.RoomClient
import com.faraphel.tasks_valider.connectivity.wifi_p2p.WifiP2pHelper import com.faraphel.tasks_valider.connectivity.wifi_p2p.WifiP2pHelper
import com.faraphel.tasks_valider.ui.screen.tasks.TaskGroupScreen
import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget
@ -17,17 +23,67 @@ import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidge
@Composable @Composable
fun RoomClientScreen(wifiP2pHelper: WifiP2pHelper) { fun RoomClientScreen(wifiP2pHelper: WifiP2pHelper) {
val peers = remember { mutableStateOf<WifiP2pDeviceList?>(null) } val peers = remember { mutableStateOf<WifiP2pDeviceList?>(null) }
val selectedDevice = remember { mutableStateOf<WifiP2pDevice?>(null) }
val connected = remember { mutableStateOf(false) }
// if the device is connected to a host, display the main screen
if (connected.value) {
TaskGroupScreen()
}
// if a device is selected, connect to it
if (selectedDevice.value != null) {
// configure the connection to point to the selected device
val config = WifiP2pConfig().apply {
deviceAddress = selectedDevice.value!!.deviceAddress
}
// try to connect
wifiP2pHelper.connect(config, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
Log.i("room", "Connection successful to the host !")
// request additional information about the connection to obtain the host IP
wifiP2pHelper.registerListener(
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION,
{ connectionInfo: WifiP2pInfo ->
val client = RoomClient(
connectionInfo.groupOwnerAddress,
9876 // TODO(Faraphel): port should be a settings
)
Thread {
client.start()
}.start()
connected.value = true
},
once = true
)
}
override fun onFailure(reason: Int) {
// TODO(Faraphel): for most of theses messages, shouldn't a toast be used instead ?
Log.e("room", "Could not connect to this host. Reason: $reason")
}
})
return
}
// let the user pick which host he wishes to connect to
Column { Column {
Text(text = "Find a Server") Text(text = "Find a Server")
WifiP2pDeviceListWidget(peers.value) // display all the devices that are owner of their group
WifiP2pDeviceListWidget(
peers = peers.value,
filter = { device: WifiP2pDevice -> device.isGroupOwner },
selectedDevice,
)
} }
// TODO(Faraphel): might be doubtful. Test with more phones, should it only run once ? refresh button ? // TODO(Faraphel): might be doubtful. Test with more phones, should it only run once ? refresh button ?
// update the list when a new device is detected
wifiP2pHelper.registerListener( wifiP2pHelper.registerListener(
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION, WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION,
{ new_peers: WifiP2pDeviceList? -> peers.value = new_peers }, { newPeers: WifiP2pDeviceList? -> peers.value = newPeers },
once = true
) )
wifiP2pHelper.discoverPeers() wifiP2pHelper.discoverPeers()
} }

View file

@ -2,22 +2,29 @@ package com.faraphel.tasks_valider.ui.screen.room
import android.net.wifi.p2p.WifiP2pManager import android.net.wifi.p2p.WifiP2pManager
import android.util.Log import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import com.faraphel.tasks_valider.connectivity.wifi_p2p.WifiP2pHelper import com.faraphel.tasks_valider.connectivity.wifi_p2p.WifiP2pHelper
import com.faraphel.tasks_valider.ui.screen.tasks.TaskGroupScreen import com.faraphel.tasks_valider.ui.screen.tasks.TaskGroupScreen
import java.net.ServerSocket
var DEFAULT_SERVER_PORT: Int = 9876
/** /**
* This screen allow the user to create a room * This screen allow the user to create a room
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RoomHostScreen(wifiP2pHelper: WifiP2pHelper) { fun RoomHostScreen(wifiP2pHelper: WifiP2pHelper) {
val expanded = remember { mutableStateOf(false) } val expanded = remember { mutableStateOf(false) }
val serverPort = remember { mutableStateOf(DEFAULT_SERVER_PORT) }
val isCreated = remember { mutableStateOf(false) } val isCreated = remember { mutableStateOf(false) }
// if the group have been created, display the group screen // if the group have been created, display the group screen
@ -26,10 +33,17 @@ fun RoomHostScreen(wifiP2pHelper: WifiP2pHelper) {
return return
} }
// ask the user settings about the room
Column { Column {
ExposedDropdownMenuBox( Box(Modifier.fillMaxWidth()) {
// dropdown button
Button(onClick = { expanded.value = !expanded.value }) {
Text(text = "Select Students List")
}
// dropdown list
DropdownMenu(
expanded = expanded.value, expanded = expanded.value,
onExpandedChange = { value -> expanded.value = !value } onDismissRequest = { expanded.value = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("ISRI") }, text = { Text("ISRI") },
@ -40,12 +54,27 @@ fun RoomHostScreen(wifiP2pHelper: WifiP2pHelper) {
onClick = {} onClick = {}
) )
} }
}
// ask the user what will be the server port
TextField(
value = serverPort.value.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { value -> serverPort.value = value.toIntOrNull() ?: DEFAULT_SERVER_PORT }
)
Button(onClick = { Button(onClick = {
// create a new WiFi-Direct group // create a new WiFi-Direct group
wifiP2pHelper.recreateGroup(object : WifiP2pManager.ActionListener { wifiP2pHelper.recreateGroup(object : WifiP2pManager.ActionListener {
override fun onSuccess() { override fun onSuccess() {
Log.i("room", "group created !") Log.i("room", "group created !")
val server = ServerSocket(serverPort.value) // TODO(Faraphel): should the port be a settings ?
Thread {
val client = server.accept() // TODO(Faraphel): should run in a thread
client.getInputStream()
}
// mark the group as created // mark the group as created
isCreated.value = true isCreated.value = true
} }

View file

@ -9,5 +9,6 @@ import androidx.compose.runtime.Composable
*/ */
@Composable @Composable
fun TaskGroupScreen() { fun TaskGroupScreen() {
// TODO(Faraphel): should handle connexion with the server
Text(text = "Task Group") Text(text = "Task Group")
} }

View file

@ -1,22 +1,38 @@
package com.faraphel.tasks_valider.ui.widgets.connectivity package com.faraphel.tasks_valider.ui.widgets.connectivity
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList import android.net.wifi.p2p.WifiP2pDeviceList
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
/** /**
* Represent a list of WiFi-Direct devices. * Represent a list of WiFi-Direct devices.
* @param peers the list of peers to represent
* @param filter a filter for the peers
* @param deviceState a state containing the selected device
*/ */
@Composable @Composable
fun WifiP2pDeviceListWidget(peers: WifiP2pDeviceList?) { fun WifiP2pDeviceListWidget(
peers: WifiP2pDeviceList?,
filter: ((WifiP2pDevice) -> Boolean)? = null,
deviceState: MutableState<WifiP2pDevice?>? = null,
) {
Text(text = "Devices (${peers?.deviceList?.size ?: 0})") Text(text = "Devices (${peers?.deviceList?.size ?: 0})")
Column { Column {
// if there are peers to display
if (peers != null) { if (peers != null) {
// for every device in the list
for (device in peers.deviceList) { for (device in peers.deviceList) {
WifiP2pDeviceWidget(device) // if the filter (if set) does not apply to this device, ignore it
if (filter != null && !filter(device))
continue
// create a new object for the device
WifiP2pDeviceWidget(device, deviceState)
} }
} }
} }

View file

@ -3,24 +3,42 @@ package com.faraphel.tasks_valider.ui.widgets.connectivity
import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDevice
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.graphics.Color
/**
* A widget that represent a WiFi-Direct device.
* @param device the device that should be represented
* @param deviceState a state that will be updated to the device when it is selected
*/
@Composable @Composable
fun WifiP2pDeviceWidget(device: WifiP2pDevice) { fun WifiP2pDeviceWidget(device: WifiP2pDevice, deviceState: MutableState<WifiP2pDevice?>? = null) {
Button(onClick = { if (deviceState != null) deviceState.value = device }) {
Column { Column {
Row { Row {
Text(text = "Name: ") Text(text = "Name: ")
Text(text = device.deviceName) Text(text = device.deviceName)
} }
Row {
Text(text = "Address: ")
Text(text = device.deviceAddress)
}
Row { Row {
Text(text = "Is Owner: ") Text(text = "Is Owner: ")
Text(text = device.isGroupOwner.toString()) Text(text = device.isGroupOwner.toString())
} }
Row {
Text(text = "Address: ", color = Color.LightGray)
Text(text = device.deviceAddress, color = Color.LightGray)
}
Row {
Text(text = "Primary Type: ", color = Color.Green)
Text(text = device.primaryDeviceType, color = Color.Green)
}
Row {
Text(text = "Status: ", color = Color.Yellow)
Text(text = device.status.toString(), color = Color.Yellow)
}
}
} }
} }

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.ui.widgets package com.faraphel.tasks_valider.ui.widgets.tasks
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text import androidx.compose.material3.Text

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.ui.widgets package com.faraphel.tasks_valider.ui.widgets.tasks
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text import androidx.compose.material3.Text

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.ui.widgets package com.faraphel.tasks_valider.ui.widgets.tasks
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column

View file

@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "8.2.2" apply false id("com.android.application") version "8.2.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
id("com.google.devtools.ksp") version "1.9.21-1.0.15" apply false id("com.google.devtools.ksp") version "1.9.21-1.0.15" apply false
kotlin("android") version "1.9.23" apply false
kotlin("plugin.serialization") version "1.9.23" apply false
} }