[WIP] linked client and server system with the UI

This commit is contained in:
Faraphel 2024-05-10 22:31:11 +02:00
parent c9334c543b
commit bc8dd0f859
12 changed files with 202 additions and 86 deletions

View file

@ -1,4 +1,3 @@
<?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="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">

View file

@ -28,6 +28,7 @@
<!-- Applications --> <!-- Applications -->
<!-- NOTE: usesCleartextTraffic is enabled because of the API system using simple HTTP -->
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -38,6 +39,7 @@
android:supportsRtl="true" android:supportsRtl="true"
tools:ignore="RtlEnabled" tools:ignore="RtlEnabled"
android:theme="@style/Theme.Tasksvalider" android:theme="@style/Theme.Tasksvalider"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View file

@ -1,49 +1,100 @@
package com.faraphel.tasks_valider.connectivity.task package com.faraphel.tasks_valider.connectivity.task
import android.util.Log import okhttp3.HttpUrl
import com.google.gson.Gson import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.net.InetAddress import okhttp3.RequestBody.Companion.toRequestBody
import java.net.URL
/** /**
* A client to handle the room connection. * A client to handle the room connection.
* @param address the address of the server * @param address the address of the server
* @param port the port of the server * @param port the port of the server
* @param baseCookies list of cookies to use (optional)
*/ */
class TaskClient( class TaskClient(
private val address: InetAddress, private val address: String,
private val port: Int private val port: Int,
) : Thread() { private val baseCookies: List<okhttp3.Cookie> = listOf()
private val client = OkHttpClient() ) {
private val jsonParser = Gson() private val baseUrl = "http://$address:$port"
private val client = OkHttpClient().newBuilder().cookieJar(
// TODO(Faraphel): should be moved into another object
object : okhttp3.CookieJar {
private val cookies = baseCookies.toMutableList() ///< list of cookies
constructor(address: String, port: Int) : this(InetAddress.getByName(address), port) override fun loadForRequest(url: HttpUrl): List<okhttp3.Cookie> {
return this.cookies
}
override fun saveFromResponse(url: HttpUrl, cookies: List<okhttp3.Cookie>) {
this.cookies.addAll(cookies)
}
}
).build()
override fun run() { // TODO(Faraphel): automatically convert content to the correct type ?
Log.i("room-client", "started !")
// send a request to the server /**
val request = okhttp3.Request.Builder() * Return a basic request to the server
.url(URL("http://${address.hostAddress}:$port")) * @param endpoint the endpoint of the server
*/
private fun baseRequestBuilder(endpoint: String): okhttp3.Request.Builder =
okhttp3.Request.Builder().url("$baseUrl/$endpoint")
/**
* Run a HEAD request
* @param endpoint the endpoint of the server
*/
fun head(endpoint: String): okhttp3.Request =
this.baseRequestBuilder(endpoint).head().build()
/**
* Run a GET request
* @param endpoint the endpoint of the server
*/
fun get(endpoint: String): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.get()
.build() .build()
).execute()
// get the response /**
val response = client.newCall(request).execute() * Run a POST request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
fun post(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.post(content.toRequestBody(type.toMediaType()))
.build()
).execute()
// check if the response is successful /**
if (!response.isSuccessful) { * Run a PATCH request
Log.e("room-client", "could not connect to the server") * @param endpoint the endpoint of the server
return * @param content the content of the request
} * @param type the type of the content
Log.i("room-client", "connected to the server") */
fun patch(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.patch(content.toRequestBody(type.toMediaType()))
.build()
).execute()
// parse the response /**
val body = response.body.string() * Run a DELETE request
val data = jsonParser.fromJson(body, Map::class.java) * @param endpoint the endpoint of the server
* @param content the content of the request
// print the data * @param type the type of the content
data.forEach { (key, value) -> Log.d("room-client", "$key: $value") } */
} fun delete(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response =
this.client.newCall(
this.baseRequestBuilder(endpoint)
.delete(content.toRequestBody(type.toMediaType()))
.build()
).execute()
} }

View file

@ -1,6 +1,8 @@
package com.faraphel.tasks_valider.connectivity.task package com.faraphel.tasks_valider.connectivity.task
import com.faraphel.tasks_valider.connectivity.task.api.TaskSessionManagerApi import com.faraphel.tasks_valider.connectivity.task.api.TaskSessionManagerApi
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager
import com.faraphel.tasks_valider.database.TaskDatabase import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.api.TaskDatabaseApi import com.faraphel.tasks_valider.database.api.TaskDatabaseApi
@ -16,11 +18,36 @@ class TaskServer(
private val port: Int, private val port: Int,
private val database: TaskDatabase private val database: TaskDatabase
) : NanoHTTPD(port) { ) : NanoHTTPD(port) {
companion object {
private val TASK_SESSION_ADMIN = TaskSession( ///< the admin default session
role = TaskRole.ADMIN
)
}
private val sessionManager = TaskSessionManager() ///< the session manager private val sessionManager = TaskSessionManager() ///< the session manager
private val adminSessionId = this.sessionManager.newSessionData(TASK_SESSION_ADMIN) ///< default admin session id
private val sessionManagerApi = TaskSessionManagerApi(this.sessionManager) ///< the api of the session manager private val sessionManagerApi = TaskSessionManagerApi(this.sessionManager) ///< the api of the session manager
private val databaseApi = TaskDatabaseApi(this.database) ///< the api of the database private val databaseApi = TaskDatabaseApi(this.database) ///< the api of the database
/**
* Return a new client that can be used by the admin
*/
fun getClientAdmin(): TaskClient {
// create the session cookie for the admin
val cookieSession = okhttp3.Cookie.Builder()
.domain("localhost")
.name("sessionId")
.value(adminSessionId)
.build()
// create a new client
return TaskClient(
"localhost",
this.port,
listOf(cookieSession)
)
}
/** /**
* Handle an API request * Handle an API request
* @param httpSession the http session * @param httpSession the http session
@ -30,7 +57,7 @@ class TaskServer(
val taskSession = this.sessionManager.getOrCreateSessionData(httpSession) val taskSession = this.sessionManager.getOrCreateSessionData(httpSession)
// parse the url // parse the url
val uri: String = httpSession.uri.substring(1) // remove the first slash val uri: String = httpSession.uri.trim('/')
val path = uri.split("/").toMutableList() val path = uri.split("/").toMutableList()
// get the type of the request from the uri // get the type of the request from the uri

View file

@ -6,6 +6,7 @@ import androidx.room.PrimaryKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity import com.faraphel.tasks_valider.database.entities.base.BaseEntity
// TODO(Faraphel): should be renamed to TeamEntity
@Entity(tableName = "groups") @Entity(tableName = "groups")
data class GroupEntity ( data class GroupEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0,

View file

@ -1,5 +1,6 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet.client package com.faraphel.tasks_valider.ui.screen.communication.internet.client
import android.app.Activity
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -19,11 +20,11 @@ import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
@Composable @Composable
fun CommunicationInternetClientScreen() { fun CommunicationInternetClientScreen(activity: Activity) {
val client = remember { mutableStateOf<TaskClient?>(null) } val client = remember { mutableStateOf<TaskClient?>(null) }
if (client.value == null) CommunicationInternetClientContent(client) if (client.value == null) CommunicationInternetClientContent(client)
else TaskGroupScreen() else TaskGroupScreen(activity, client.value!!)
} }
@ -56,7 +57,6 @@ fun CommunicationInternetClientContent(client: MutableState<TaskClient?>) {
Button(onClick = { Button(onClick = {
// TODO(Faraphel): check if the server is reachable // TODO(Faraphel): check if the server is reachable
client.value = TaskClient(serverAddress.value, serverPort.intValue) client.value = TaskClient(serverAddress.value, serverPort.intValue)
client.value!!.start()
}) { }) {
Text("Connect") Text("Connect")
} }

View file

@ -6,7 +6,6 @@ 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.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.activity
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@ -19,15 +18,9 @@ fun CommunicationInternetScreen(activity: Activity) {
val controller = rememberNavController() val controller = rememberNavController()
NavHost(navController = controller, startDestination = "mode") { NavHost(navController = controller, startDestination = "mode") {
composable("mode") { composable("mode") { CommunicationInternetSelectContent(controller) }
CommunicationInternetSelectContent(controller) composable("client") { CommunicationInternetClientScreen(activity) }
} composable("server") { CommunicationInternetServerScreen(activity) }
composable("client") {
CommunicationInternetClientScreen()
}
composable("server") {
CommunicationInternetServerScreen(activity)
}
} }
} }
@ -36,12 +29,8 @@ fun CommunicationInternetScreen(activity: Activity) {
fun CommunicationInternetSelectContent(controller: NavController) { fun CommunicationInternetSelectContent(controller: NavController) {
Column { Column {
// client mode // client mode
Button(onClick = { controller.navigate("client") }) { Button(onClick = { controller.navigate("client") }) { Text("Client") }
Text("Client")
}
// server mode // server mode
Button(onClick = { controller.navigate("server") }) { Button(onClick = { controller.navigate("server") }) { Text("Server") }
Text("Server")
}
} }
} }

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.room.Room import androidx.room.Room
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.TaskServer import com.faraphel.tasks_valider.connectivity.task.TaskServer
import com.faraphel.tasks_valider.database.TaskDatabase import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT
@ -25,17 +26,17 @@ import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
@Composable @Composable
fun CommunicationInternetServerScreen(activity: Activity) { fun CommunicationInternetServerScreen(activity: Activity) {
val server = remember { mutableStateOf<TaskServer?>(null)} val client = remember { mutableStateOf<TaskClient?>(null) }
// if the server is not created, prompt the user for the server configuration // if the server is not created, prompt the user for the server configuration
if (server.value == null) CommunicationInternetServerContent(activity, server) if (client.value == null) CommunicationInternetServerContent(activity, client)
// else, go to the base tasks screen // else, go to the base tasks screen
else TaskGroupScreen() else TaskGroupScreen(activity, client.value!!)
} }
@Composable @Composable
fun CommunicationInternetServerContent(activity: Activity, server: MutableState<TaskServer?>) { fun CommunicationInternetServerContent(activity: Activity, client: MutableState<TaskClient?>) {
val expandedStudentList = remember { mutableStateOf(false) } val expandedStudentList = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) } val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
@ -84,8 +85,13 @@ fun CommunicationInternetServerContent(activity: Activity, server: MutableState<
// Create the server // Create the server
Log.i("room-server", "creating the server") Log.i("room-server", "creating the server")
server.value = TaskServer(serverPort.intValue, database) Thread { // a thread is used for networking
server.value!!.start() val server = TaskServer(serverPort.intValue, database)
server.start()
// Get the client from the server
client.value = server.getClientAdmin()
}.start()
}) { }) {
Text("Create") Text("Create")
} }

View file

@ -1,5 +1,6 @@
package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.client package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.client
import android.app.Activity
import android.net.wifi.p2p.WifiP2pConfig import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pDevice import android.net.wifi.p2p.WifiP2pDevice
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -8,18 +9,18 @@ import androidx.compose.runtime.MutableState
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.bwf.BwfManager import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget
@Composable @Composable
fun CommunicationWifiP2pClientScreen(bwfManager: BwfManager) { fun CommunicationWifiP2pClientScreen(activity: Activity, bwfManager: BwfManager) {
val selectedDevice = remember { mutableStateOf<WifiP2pDevice?>(null) } val selectedDevice = remember { mutableStateOf<WifiP2pDevice?>(null) }
val isConnected = remember { mutableStateOf(false) } val isConnected = remember { mutableStateOf(false) }
// if connected, show the task group screen // if connected, show the task group screen
if (isConnected.value) { if (isConnected.value) {
TaskGroupScreen() // TaskGroupScreen(activity, null)
// TODO(Faraphel): finish the connection
return return
} }

View file

@ -19,15 +19,9 @@ fun CommunicationWifiP2pScreen(activity: Activity, bwfManager: BwfManager) {
val controller = rememberNavController() val controller = rememberNavController()
NavHost(navController = controller, startDestination = "mode") { NavHost(navController = controller, startDestination = "mode") {
composable("mode") { composable("mode") { CommunicationWifiP2pSelectContent(controller) }
CommunicationWifiP2pSelectContent(controller) composable("client") { CommunicationWifiP2pClientScreen(activity, bwfManager) }
} composable("server") { CommunicationWifiP2pServerScreen(activity, bwfManager) }
composable("client") {
CommunicationWifiP2pClientScreen(bwfManager)
}
composable("server") {
CommunicationWifiP2pServerScreen(activity, bwfManager)
}
} }
} }
@ -36,12 +30,8 @@ fun CommunicationWifiP2pScreen(activity: Activity, bwfManager: BwfManager) {
fun CommunicationWifiP2pSelectContent(controller: NavController) { fun CommunicationWifiP2pSelectContent(controller: NavController) {
Column { Column {
// client mode // client mode
Button(onClick = { controller.navigate("client") }) { Button(onClick = { controller.navigate("client") }) { Text("Client") }
Text("Client")
}
// server mode // server mode
Button(onClick = { controller.navigate("server") }) { Button(onClick = { controller.navigate("server") }) { Text("Server") }
Text("Server")
}
} }
} }

View file

@ -1,6 +1,7 @@
package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server
import android.app.Activity import android.app.Activity
import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -16,6 +17,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.room.Room import androidx.room.Room
import com.faraphel.tasks_valider.connectivity.bwf.BwfManager import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.TaskServer import com.faraphel.tasks_valider.connectivity.task.TaskServer
import com.faraphel.tasks_valider.database.TaskDatabase import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT
@ -25,12 +27,12 @@ import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
@Composable @Composable
fun CommunicationWifiP2pServerScreen(activity: Activity, bwfManager: BwfManager) { fun CommunicationWifiP2pServerScreen(activity: Activity, bwfManager: BwfManager) {
val server = remember { mutableStateOf<TaskServer?>(null)} val client = remember { mutableStateOf<TaskClient?>(null) }
// if the server is not created, prompt the user for the server configuration // if the server is not created, prompt the user for the server configuration
if (server.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, server) if (client.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, client)
// else, go to the base tasks screen // else, go to the base tasks screen
else TaskGroupScreen() else TaskGroupScreen(activity, client.value!!)
} }
@ -38,7 +40,7 @@ fun CommunicationWifiP2pServerScreen(activity: Activity, bwfManager: BwfManager)
fun CommunicationWifiP2pServerContent( fun CommunicationWifiP2pServerContent(
activity: Activity, activity: Activity,
bwfManager: BwfManager, bwfManager: BwfManager,
server: MutableState<TaskServer?> client: MutableState<TaskClient?>
) { ) {
val expandedStudentList = remember { mutableStateOf(false) } val expandedStudentList = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) } val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
@ -90,8 +92,12 @@ fun CommunicationWifiP2pServerContent(
bwfManager.recreateGroup { bwfManager.recreateGroup {
// Create the server // Create the server
server.value = TaskServer(serverPort.intValue, database) Log.i("room-server", "creating the server")
server.value!!.start() val server = TaskServer(serverPort.intValue, database)
server.start()
// Get the client from the server
client.value = server.getClientAdmin()
} }
}) { }) {
Text("Create") Text("Create")

View file

@ -1,14 +1,58 @@
package com.faraphel.tasks_valider.ui.screen.task package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import android.widget.Toast
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.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.database.entities.TaskGroupEntity
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
val jsonParser = Gson()
/** /**
* This screen let the user decide which student team he wants to interact with * This screen let the user decide which student team he wants to interact with
* @param client an HTTP client that can communicate with the server
*/ */
@Composable @Composable
fun TaskGroupScreen() { fun TaskGroupScreen(activity: Activity, client: TaskClient) {
// TODO(Faraphel): should handle connexion with the server val groups = remember { mutableStateOf<List<TaskGroupEntity>?>(null) }
// title
Text(text = "Task Group") Text(text = "Task Group")
// if the groups are not yet defined, refresh the list
if (groups.value == null) {
Thread { refreshGroups(activity, client, groups) }.start()
return
}
// if the groups have already been defined, display them
for (group in groups.value!!) {
Text(text = group.toString())
}
}
fun refreshGroups(activity: Activity, client: TaskClient, groups: MutableState<List<TaskGroupEntity>?>) {
// try to obtain the list of groups
val response = client.get("entities/group")
// in case of error, notify it
if (!response.isSuccessful) {
Toast.makeText(activity, response.message, Toast.LENGTH_LONG).show()
return
}
// parse the list of groups
groups.value = jsonParser.fromJson(
response.body.toString(),
object : TypeToken<List<TaskGroupEntity>>(){}
)
} }