[WIP] using JSON and HTTP server / client to communicate between the devices

This commit is contained in:
Faraphel 2024-05-05 14:24:05 +02:00
parent 2637f2fe8b
commit a604e01c12
22 changed files with 233 additions and 197 deletions

View file

@ -4,34 +4,36 @@
<value>
<entry key="app">
<State>
<targetSelectedWithDropDown>
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\RC606\.android\avd\Small_Phone_API_26.avd" />
<type value="SERIAL_NUMBER" />
<value value="2XJDU17923000406" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-05-04T10:43:32.941497Z" />
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-05-05T12:21:32.176293Z" />
<runningDeviceTargetsSelectedWithDialog>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\RC606\.android\avd\Small_Phone_API_34.avd" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetsSelectedWithDialog>
<targetsSelectedWithDialog>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\RC606\.android\avd\Small_Phone_API_26.avd" />
</Key>
</deviceKey>
</Target>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\RC606\.android\avd\Small_Phone_API_26_-_2.avd" />
<value value="C:\Users\RC606\.android\avd\Small_Phone_API_34_-_2.avd" />
</Key>
</deviceKey>
</Target>

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

@ -63,6 +63,9 @@ dependencies {
implementation("androidx.room:room-ktx:2.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("androidx.navigation:navigation-compose:2.7.7")
implementation("org.nanohttpd:nanohttpd:2.3.1")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.okhttp3:okhttp-android:5.0.0-alpha.14")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View file

@ -7,7 +7,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import com.faraphel.tasks_valider.database.Database
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.ui.screen.communication.CommunicationScreen
@ -15,7 +15,7 @@ class MainActivity : ComponentActivity() {
private var bwfManager: BwfManager? = null ///< the WiFi-Direct helper
companion object {
private lateinit var database: Database ///< the database manager
private lateinit var database: TaskDatabase ///< the database manager
}
@RequiresApi(Build.VERSION_CODES.O)

View file

@ -1,29 +0,0 @@
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

@ -1,10 +0,0 @@
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

@ -1,43 +0,0 @@
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
) : Thread() {
private val server = Socket()
constructor(address: String, port: Int) : this(InetAddress.getByName(address), port)
override fun run() {
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

@ -1,55 +0,0 @@
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
) : Thread() {
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
*/
override fun run() {
while (!server.isClosed) {
val client = server.accept()
client.soTimeout = timeout // set the timeout for the communication
this.handleClient(client)
}
}
/**
* Close the server
*/
fun close() {
server.close()
}
}

View file

@ -0,0 +1,49 @@
package com.faraphel.tasks_valider.connectivity.task
import android.util.Log
import com.google.gson.Gson
import okhttp3.OkHttpClient
import java.net.InetAddress
import java.net.URL
/**
* A client to handle the room connection.
* @param address the address of the server
* @param port the port of the server
*/
class TaskClient(
private val address: InetAddress,
private val port: Int
) : Thread() {
private val client = OkHttpClient()
private val jsonParser = Gson()
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
}
Log.i("room-client", "connected to the server")
// parse the response
val body = response.body.string()
val data = jsonParser.fromJson(body, Map::class.java)
// print the data
data.forEach { (key, value) -> Log.d("room-client", "$key: $value") }
}
}

View file

@ -0,0 +1,70 @@
package com.faraphel.tasks_valider.connectivity.task
import android.util.Log
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.entities.Task
import com.google.gson.Gson
import fi.iki.elonen.NanoHTTPD
/**
* A server to handle the task API to allow clients to interact with the database.
* @param port the port of the server
* @param database the database to interact with
*/
class TaskServer(
private val port: Int,
private val database: TaskDatabase
) : NanoHTTPD(port) {
private val jsonParser = Gson()
override fun serve(session: IHTTPSession): Response {
val method: Method = session.method
val uri: String = session.uri
// remove the first slash
val daoName: String = uri.substring(1)
// handle the request
when (method) {
// get the data from the database
Method.GET -> {
return newFixedLengthResponse(
Response.Status.OK,
"application/json",
jsonParser.toJson( database.taskDao().getAll() )
)
}
// insert the data into the database
Method.POST -> {
val task = jsonParser.fromJson(
session.inputStream.bufferedReader(),
Task::class.java
)
database.taskDao().insert(task)
return newFixedLengthResponse(
Response.Status.CREATED,
"application/json",
jsonParser.toJson(task)
)
}
// other methods are not allowed
else -> {
return newFixedLengthResponse(
Response.Status.METHOD_NOT_ALLOWED,
"text/plain",
"Method not allowed"
)
}
// TODO(Faraphel): implement a permission system
}
}
/**
* Start the server with the default configuration
*/
override fun start() {
super.start(SOCKET_READ_TIMEOUT, false)
}
}

View file

@ -1,9 +1,6 @@
package com.faraphel.tasks_valider.database
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.room.Database
import androidx.room.DatabaseConfiguration
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.faraphel.tasks_valider.database.converters.InstantConverter
@ -13,15 +10,19 @@ import com.faraphel.tasks_valider.database.dao.StudentDao
import com.faraphel.tasks_valider.database.dao.TaskDao
import com.faraphel.tasks_valider.database.dao.TaskGroupDao
import com.faraphel.tasks_valider.database.dao.TeacherDao
import com.faraphel.tasks_valider.database.dao.base.BaseDao
import com.faraphel.tasks_valider.database.entities.Group
import com.faraphel.tasks_valider.database.entities.GroupStudent
import com.faraphel.tasks_valider.database.entities.Student
import com.faraphel.tasks_valider.database.entities.Task
import com.faraphel.tasks_valider.database.entities.TaskGroup
import com.faraphel.tasks_valider.database.entities.Teacher
import java.time.Instant
/**
* The database for the tasks application.
* Contains the entities and the relations between them.
*/
@Database(
entities = [
Group::class,
@ -37,7 +38,7 @@ import java.time.Instant
@TypeConverters(
InstantConverter::class
)
abstract class Database : RoomDatabase() {
abstract class TaskDatabase : RoomDatabase() {
// entities
abstract fun groupDao(): GroupDao
abstract fun studentDao(): StudentDao
@ -47,4 +48,20 @@ abstract class Database : RoomDatabase() {
// relations
abstract fun groupStudentDao(): GroupStudentDao
abstract fun taskGroupDao(): TaskGroupDao
/**
* Get the DAO from the name of the dao.
*/
@Suppress("UNCHECKED_CAST")
fun <Entity> daoFromName(name: String): BaseDao<Entity>? {
return when (name) {
"group" -> groupDao()
"student" -> studentDao()
"teacher" -> teacherDao()
"task" -> taskDao()
"group_student" -> groupStudentDao()
"task_group" -> taskGroupDao()
else -> null
} as BaseDao<Entity>?
}
}

View file

@ -11,16 +11,16 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType
import com.faraphel.tasks_valider.connectivity.room.RoomClient
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_ADDRESS
import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.tasks.TaskGroupScreen
import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
@Composable
fun CommunicationInternetClientScreen() {
val client = remember { mutableStateOf<RoomClient?>(null) }
val client = remember { mutableStateOf<TaskClient?>(null) }
if (client.value == null) CommunicationInternetClientContent(client)
else TaskGroupScreen()
@ -28,7 +28,7 @@ fun CommunicationInternetClientScreen() {
@Composable
fun CommunicationInternetClientContent(client: MutableState<RoomClient?>) {
fun CommunicationInternetClientContent(client: MutableState<TaskClient?>) {
val serverAddress = remember { mutableStateOf(DEFAULT_SERVER_ADDRESS) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
@ -55,7 +55,7 @@ fun CommunicationInternetClientContent(client: MutableState<RoomClient?>) {
Button(onClick = {
// TODO(Faraphel): check if the server is reachable
client.value = RoomClient(serverAddress.value, serverPort.intValue)
client.value = TaskClient(serverAddress.value, serverPort.intValue)
client.value!!.start()
}) {
Text("Connect")

View file

@ -1,10 +1,12 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet
import android.app.Activity
import androidx.compose.foundation.layout.Column
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
@ -13,7 +15,7 @@ import com.faraphel.tasks_valider.ui.screen.communication.internet.server.Commun
@Composable
fun CommunicationInternetScreen() {
fun CommunicationInternetScreen(activity: Activity) {
val controller = rememberNavController()
NavHost(navController = controller, startDestination = "mode") {
@ -24,7 +26,7 @@ fun CommunicationInternetScreen() {
CommunicationInternetClientScreen()
}
composable("server") {
CommunicationInternetServerScreen()
CommunicationInternetServerScreen(activity)
}
}
}

View file

@ -1,5 +1,7 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet.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
@ -13,26 +15,27 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.KeyboardType
import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import com.faraphel.tasks_valider.connectivity.room.RoomServer
import androidx.room.Room
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
import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.tasks.TaskGroupScreen
import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
@Composable
fun CommunicationInternetServerScreen() {
val server = remember { mutableStateOf<RoomServer?>(null)}
fun CommunicationInternetServerScreen(activity: Activity) {
val server = remember { mutableStateOf<TaskServer?>(null)}
// if the server is not created, prompt the user for the server configuration
if (server.value == null) CommunicationInternetServerContent(server)
if (server.value == null) CommunicationInternetServerContent(activity, server)
// else, go to the base tasks screen
else TaskGroupScreen()
}
@Composable
fun CommunicationInternetServerContent(server: MutableState<RoomServer?>) {
fun CommunicationInternetServerContent(activity: Activity, server: MutableState<TaskServer?>) {
val expandedStudentList = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
@ -69,9 +72,20 @@ fun CommunicationInternetServerContent(server: MutableState<RoomServer?>) {
)
Button(onClick = {
server.value = RoomServer(serverPort.intValue)
// Reset the database | TODO(Faraphel): only for testing purpose
activity.deleteDatabase("local")
// Create the database
val database = Room.databaseBuilder(
activity,
TaskDatabase::class.java,
"local"
).build()
// Create the server
Log.i("room-server", "creating the server")
server.value = TaskServer(serverPort.intValue, database)
server.value!!.start()
// TODO(Faraphel): go to the base tasks screen
}) {
Text("Create")
}

View file

@ -35,11 +35,11 @@ fun CommunicationScreen(activity: Activity) {
CommunicationSelectContent(controller, activity)
}
composable("internet") {
CommunicationInternetScreen()
CommunicationInternetScreen(activity)
}
composable("wifi-p2p") {
val bwfManager = BwfManager.fromActivity(activity)
CommunicationWifiP2pScreen(bwfManager)
CommunicationWifiP2pScreen(activity, bwfManager)
}
}
}

View file

@ -8,7 +8,7 @@ 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.tasks.TaskGroupScreen
import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget
@ -23,6 +23,7 @@ fun CommunicationWifiP2pClientScreen(bwfManager: BwfManager) {
return
}
// if the device is selected but not connected, try to connect
if (selectedDevice.value != null) {
// TODO(Faraphel): error handling

View file

@ -1,5 +1,6 @@
package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p
import android.app.Activity
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
@ -9,14 +10,12 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwf.BwfManager
import com.faraphel.tasks_valider.ui.screen.communication.internet.CommunicationInternetScreen
import com.faraphel.tasks_valider.ui.screen.communication.internet.server.CommunicationInternetServerScreen
import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.client.CommunicationWifiP2pClientScreen
import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server.CommunicationWifiP2pServerScreen
@Composable
fun CommunicationWifiP2pScreen(bwfManager: BwfManager) {
fun CommunicationWifiP2pScreen(activity: Activity, bwfManager: BwfManager) {
val controller = rememberNavController()
NavHost(navController = controller, startDestination = "mode") {
@ -27,7 +26,7 @@ fun CommunicationWifiP2pScreen(bwfManager: BwfManager) {
CommunicationWifiP2pClientScreen(bwfManager)
}
composable("server") {
CommunicationWifiP2pServerScreen(bwfManager)
CommunicationWifiP2pServerScreen(activity, bwfManager)
}
}
}

View file

@ -1,5 +1,6 @@
package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server
import android.app.Activity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
@ -13,19 +14,21 @@ import androidx.compose.runtime.mutableIntStateOf
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.bwf.BwfManager
import com.faraphel.tasks_valider.connectivity.room.RoomServer
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
import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.tasks.TaskGroupScreen
import com.faraphel.tasks_valider.ui.screen.task.TaskGroupScreen
@Composable
fun CommunicationWifiP2pServerScreen(bwfManager: BwfManager) {
val server = remember { mutableStateOf<RoomServer?>(null)}
fun CommunicationWifiP2pServerScreen(activity: Activity, bwfManager: BwfManager) {
val server = remember { mutableStateOf<TaskServer?>(null)}
// if the server is not created, prompt the user for the server configuration
if (server.value == null) CommunicationWifiP2pServerContent(bwfManager, server)
if (server.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, server)
// else, go to the base tasks screen
else TaskGroupScreen()
}
@ -33,8 +36,9 @@ fun CommunicationWifiP2pServerScreen(bwfManager: BwfManager) {
@Composable
fun CommunicationWifiP2pServerContent(
activity: Activity,
bwfManager: BwfManager,
server: MutableState<RoomServer?>
server: MutableState<TaskServer?>
) {
val expandedStudentList = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
@ -72,8 +76,21 @@ fun CommunicationWifiP2pServerContent(
)
Button(onClick = {
// TODO(Faraphel): should be merged with the internet server
// Reset the database | TODO(Faraphel): only for testing purpose
activity.deleteDatabase("local")
// Create the database
val database = Room.databaseBuilder(
activity,
TaskDatabase::class.java,
"local"
).build()
bwfManager.recreateGroup {
server.value = RoomServer(serverPort.intValue)
// Create the server
server.value = TaskServer(serverPort.intValue, database)
server.value!!.start()
}
}) {

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.ui.screen.tasks
package com.faraphel.tasks_valider.ui.screen.task
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.ui.widgets.tasks
package com.faraphel.tasks_valider.ui.widgets.task
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -11,12 +11,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.faraphel.tasks_valider.database.Database
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.entities.TaskGroup
@Composable
fun WidgetTaskStudent(database: Database, taskStudent: TaskGroup) {
fun WidgetTaskStudent(database: TaskDatabase, taskStudent: TaskGroup) {
val teacherDao = database.teacherDao()
Column {