Http Server / Client communication #7

Merged
faraphel merged 24 commits from test-http into main 2024-05-17 17:22:56 +02:00
12 changed files with 202 additions and 86 deletions
Showing only changes of commit bc8dd0f859 - Show all commits

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">

View file

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

View file

@ -1,49 +1,100 @@
package com.faraphel.tasks_valider.connectivity.task
import android.util.Log
import com.google.gson.Gson
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import java.net.InetAddress
import java.net.URL
import okhttp3.RequestBody.Companion.toRequestBody
/**
* A client to handle the room connection.
* @param address the address of the server
* @param port the port of the server
* @param baseCookies list of cookies to use (optional)
*/
class TaskClient(
private val address: InetAddress,
private val port: Int
) : Thread() {
private val client = OkHttpClient()
private val jsonParser = Gson()
private val address: String,
private val port: Int,
private val baseCookies: List<okhttp3.Cookie> = listOf()
) {
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 run() {
Log.i("room-client", "started !")
// send a request to the server
val request = okhttp3.Request.Builder()
.url(URL("http://${address.hostAddress}:$port"))
.build()
// get the response
val response = client.newCall(request).execute()
// check if the response is successful
if (!response.isSuccessful) {
Log.e("room-client", "could not connect to the server")
return
override fun loadForRequest(url: HttpUrl): List<okhttp3.Cookie> {
return this.cookies
}
override fun saveFromResponse(url: HttpUrl, cookies: List<okhttp3.Cookie>) {
this.cookies.addAll(cookies)
}
}
Log.i("room-client", "connected to the server")
).build()
// parse the response
val body = response.body.string()
val data = jsonParser.fromJson(body, Map::class.java)
// TODO(Faraphel): automatically convert content to the correct type ?
// print the data
data.forEach { (key, value) -> Log.d("room-client", "$key: $value") }
}
/**
* Return a basic request to the server
* @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()
).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()
/**
* Run a PATCH request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
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()
/**
* Run a DELETE request
* @param endpoint the endpoint of the server
* @param content the content of the request
* @param type the type of the content
*/
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
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.database.TaskDatabase
import com.faraphel.tasks_valider.database.api.TaskDatabaseApi
@ -16,11 +18,36 @@ class TaskServer(
private val port: Int,
private val database: TaskDatabase
) : 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 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 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
* @param httpSession the http session
@ -30,7 +57,7 @@ class TaskServer(
val taskSession = this.sessionManager.getOrCreateSessionData(httpSession)
// 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()
// 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
// TODO(Faraphel): should be renamed to TeamEntity
@Entity(tableName = "groups")
data class GroupEntity (
@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
import android.app.Activity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
@ -19,11 +20,11 @@ import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
@Composable
fun CommunicationInternetClientScreen() {
fun CommunicationInternetClientScreen(activity: Activity) {
val client = remember { mutableStateOf<TaskClient?>(null) }
if (client.value == null) CommunicationInternetClientContent(client)
else TaskGroupScreen()
else TaskGroupScreen(activity, client.value!!)
}
@ -56,7 +57,6 @@ fun CommunicationInternetClientContent(client: MutableState<TaskClient?>) {
Button(onClick = {
// TODO(Faraphel): check if the server is reachable
client.value = TaskClient(serverAddress.value, serverPort.intValue)
client.value!!.start()
}) {
Text("Connect")
}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
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.WifiP2pDevice
import androidx.compose.foundation.layout.Column
@ -8,18 +9,18 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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
@Composable
fun CommunicationWifiP2pClientScreen(bwfManager: BwfManager) {
fun CommunicationWifiP2pClientScreen(activity: Activity, bwfManager: BwfManager) {
val selectedDevice = remember { mutableStateOf<WifiP2pDevice?>(null) }
val isConnected = remember { mutableStateOf(false) }
// if connected, show the task group screen
if (isConnected.value) {
TaskGroupScreen()
// TaskGroupScreen(activity, null)
// TODO(Faraphel): finish the connection
return
}

View file

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

View file

@ -1,6 +1,7 @@
package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server
import android.app.Activity
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
@ -16,6 +17,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType
import androidx.room.Room
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.database.TaskDatabase
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
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 (server.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, server)
if (client.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, client)
// 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(
activity: Activity,
bwfManager: BwfManager,
server: MutableState<TaskServer?>
client: MutableState<TaskClient?>
) {
val expandedStudentList = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
@ -90,8 +92,12 @@ fun CommunicationWifiP2pServerContent(
bwfManager.recreateGroup {
// Create the server
server.value = TaskServer(serverPort.intValue, database)
server.value!!.start()
Log.i("room-server", "creating the server")
val server = TaskServer(serverPort.intValue, database)
server.start()
// Get the client from the server
client.value = server.getClientAdmin()
}
}) {
Text("Create")

View file

@ -1,14 +1,58 @@
package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import android.widget.Toast
import androidx.compose.material3.Text
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
* @param client an HTTP client that can communicate with the server
*/
@Composable
fun TaskGroupScreen() {
// TODO(Faraphel): should handle connexion with the server
fun TaskGroupScreen(activity: Activity, client: TaskClient) {
val groups = remember { mutableStateOf<List<TaskGroupEntity>?>(null) }
// title
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>>(){}
)
}