Merge pull request 'Implemented Connection with Wi-Fi Direct and IP' (#9) from connection into main

Reviewed-on: #9
This commit is contained in:
faraphel 2024-06-30 16:01:25 +00:00
commit 033ad040b5
39 changed files with 927 additions and 447 deletions

View file

@ -1,3 +1,22 @@
# Study-M1-PDS
# Master 1 - Projet de Spécialité
(Université) - Projet De Spécialité
Ce projet consiste en une application Android permettant à des enseignants de créer des session de cours où un ensemble
d'élève est assigné à des tâches qui peuvent être validées de manière collaborative.
Voici les fonctionnalités proposées :
- Connection entre plusieurs appareils en IP ou Wi-Fi Direct
- Création de session de cours contenant une classe et des enseignants
- Authentification à l'aide du QR code présent sur les cartes étudiantes
- Support de différents sujets composés de différentes questions pour chaqu'un des élèves
- Possibilité pour un enseignant de rejoindre la connection pour valider collaborativement les tâches des élèves
- Possibilité pour un étudiant de rejoindre la connection pour vérifier les tâches validées
- Exportation des données dans un fichier `JSON`
- Validation rapide permettant de valider la tâche suivante d'un élève à l'aide de son QR code
# Build
1. Cloner le projet à l'aide de la commande `git clone https://git.faraphel.fr/study-faraphel/M1-PDS`
2. Ouvrer le dans [Android Studio](https://developer.android.com/studio?hl=fr) ou [Intellij IDEA](https://www.jetbrains.com/idea/)
3. Sélectionner votre téléphone dans le [menu de configuration] (https://www.jetbrains.com/help/idea/run-debug-configuration.html)
(l'utilisation d'émulateur n'est pas préconisé pour la fonctionnalité Wi-Fi Direct)
4. Démarrer l'application avec le bouton `Run`

View file

@ -48,7 +48,8 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Tasksvalider">
android:theme="@style/Theme.Tasksvalider"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View file

@ -1,6 +1,7 @@
package com.faraphel.tasks_valider.connectivity.bwd
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
@ -41,15 +42,16 @@ class BwdManager(
* Create a new BwfManager from an activity.
* @param activity The activity to create the manager from
*/
@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun fromActivity(activity: Activity): BwdManager {
// check if the system support WiFi-Direct
if (this.isSupported(activity)) {
if (!this.isSupported(activity)) {
Log.e("wifi-p2p", "this device does not support the WiFi-Direct feature")
throw BwdNotSupportedException()
}
// TODO(Faraphel): more check on permissions
if (
/* if (
activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
activity.checkSelfPermission(Manifest.permission.NEARBY_WIFI_DEVICES) != PackageManager.PERMISSION_GRANTED
) {
@ -59,6 +61,7 @@ class BwdManager(
PERMISSION_ACCESS_FINE_LOCATION
)
}
*/
// get the WiFi-Direct manager
val manager = activity.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager?
@ -66,9 +69,14 @@ class BwdManager(
// get the WiFi-Direct channel
val channel = manager.initialize(activity, activity.mainLooper, null)
return BwdManager(manager, channel)
// create the manager
val bwdManager = BwdManager(manager, channel)
// NOTE(Faraphel): the broadcast receiver should be registered in the activity onResume
// make the manager receive the application intents
activity.registerReceiver(bwdManager, ALL_INTENT_FILTER)
return bwdManager
}
}
@ -173,6 +181,7 @@ class BwdManager(
val stateConnectionInfo = mutableStateOf<WifiP2pInfo?>(null)
val statePeers = mutableStateOf<WifiP2pDeviceList?>(null)
override fun onReceive(context: Context?, intent: Intent?) {
// ignore empty intent
if (intent == null)

View file

@ -1,6 +1,6 @@
# Better WiFi-Direct (BWD)
# Better Wi-Fi Direct (BWD)
This package contain code to improve the base WiFi-Direct implementation.
This package contain code to improve the base Wi-Fi Direct implementation.
The base have some issue, like an abusive usage of listener, error code and events that make using it
very impractical.

View file

@ -1,5 +1,6 @@
package com.faraphel.tasks_valider.connectivity.task
import com.faraphel.tasks_valider.connectivity.task.api.TaskSessionManagerClientApi
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.*
@ -17,13 +18,19 @@ class TaskClient(
) {
private val httpClient = TaskEntityHttpClient(address, port, baseCookies)
val clientApi = ClassClientApi(httpClient)
val personApi = PersonClientApi(httpClient)
val sessionApi = SessionClientApi(httpClient)
val subjectApi = SubjectClientApi(httpClient)
val taskApi = TaskClientApi(httpClient)
val validationApi = ValidationClientApi(httpClient)
// all the entities API
class Entities(httpClient: TaskEntityHttpClient) {
val client = ClassClientApi(httpClient)
val person = PersonClientApi(httpClient)
val session = SessionClientApi(httpClient)
val subject = SubjectClientApi(httpClient)
val task = TaskClientApi(httpClient)
val validation = ValidationClientApi(httpClient)
val relationClassPersonApi = RelationClassPersonClientApi(httpClient)
val relationPersonSessionSubjectApi = RelationPersonSessionSubjectClientApi(httpClient)
}
val relationClassPerson = RelationClassPersonClientApi(httpClient)
val relationPersonSessionSubject = RelationPersonSessionSubjectClientApi(httpClient)
}
val entities = Entities(httpClient)
val session = TaskSessionManagerClientApi(httpClient)
}

View file

@ -1,6 +1,6 @@
package com.faraphel.tasks_valider.connectivity.task
import com.faraphel.tasks_valider.connectivity.task.api.TaskSessionManagerApi
import com.faraphel.tasks_valider.connectivity.task.api.TaskSessionManagerServerApi
import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.api.server.DatabaseApi
@ -24,7 +24,7 @@ class TaskServer(
) : NanoHTTPD(port) {
private val sessionManager = TaskSessionManager(adminPersonEntity) ///< the session manager
private val databaseApi = DatabaseApi(this.database, session) ///< the api of the database
private val sessionManagerApi = TaskSessionManagerApi(this.sessionManager, this.database) ///< the api of the session manager
private val sessionManagerApi = TaskSessionManagerServerApi(this.sessionManager, this.database) ///< the api of the session manager
/**
* Get the admin person entity

View file

@ -0,0 +1,58 @@
package com.faraphel.tasks_valider.connectivity.task.api
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.entities.error.HttpException
import com.faraphel.tasks_valider.utils.parser
/**
* Interface to communicate with the server session manager
*/
class TaskSessionManagerClientApi(private val client: TaskEntityHttpClient) {
/**
* Create a new session
* @param cardId the id of the user's card
* @param password the password of the user
*/
fun newFromCardId(cardId: String, password: String): TaskSession {
val response = this.client.post(
"sessions/self",
parser.toJson(mapOf(
"card_id" to cardId,
"password" to password
))
)
// in case of error, notify it
if (!response.isSuccessful)
throw HttpException(response.code)
val data = response.body.string()
// parse the result
return parser.fromJson(
data,
TaskSession::class.java
)
}
/**
* Get the current session
*/
fun getSelf(): TaskSession {
val response = this.client.get("sessions/self")
// in case of error, notify it
if (!response.isSuccessful)
throw HttpException(response.code)
// parse the result
return parser.fromJson(
response.body.string(),
TaskSession::class.java
)
}
}

View file

@ -4,6 +4,8 @@ import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission
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.entities.PersonEntity
import com.faraphel.tasks_valider.utils.getBody
import com.faraphel.tasks_valider.utils.parser
import com.google.gson.reflect.TypeToken
import fi.iki.elonen.NanoHTTPD
@ -12,7 +14,7 @@ import fi.iki.elonen.NanoHTTPD
/**
* the HTTP API for the session manager
*/
class TaskSessionManagerApi(
class TaskSessionManagerServerApi(
private val sessionManager: TaskSessionManager,
private val database: TaskDatabase
) {
@ -72,25 +74,44 @@ class TaskSessionManagerApi(
NanoHTTPD.Method.POST -> {
// get the user identifiers
val identifiers: Map<String, String> = parser.fromJson(
httpSession.inputStream.bufferedReader().readText(),
httpSession.getBody(),
object : TypeToken<Map<String, String>>() {}.type
)
val person: PersonEntity
// check for the id
if (!identifiers.contains("id"))
if (identifiers.contains("id")) {
// get the id of the user (if invalid, return an error)
val personId: Long = identifiers["id"]!!.toLongOrNull()
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Invalid id"
)
// check if the identifiers are correct
person = this.database.personDao().getById(personId)
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"No person with this id"
)
} else if (identifiers.contains("card_id")) {
// check if the identifiers are correct
person = this.database.personDao().getByCardId(identifiers["card_id"]!!)
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"No person with this id"
)
} else {
return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Missing id"
)
// get the id of the user (if invalid, return an error)
val personId: Long = identifiers["id"]!!.toLongOrNull()
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"Invalid id"
"Missing id or card_id"
)
}
// check for the password
if (!identifiers.contains("password"))
@ -100,14 +121,6 @@ class TaskSessionManagerApi(
"Missing password"
)
// check if the identifiers are correct
val person = this.database.personDao().getById(personId)
?: return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain",
"No person with this id"
)
// check if the password is correct
if (!person.checkPassword(identifiers["password"]!!))
return NanoHTTPD.newFixedLengthResponse(
@ -119,11 +132,11 @@ class TaskSessionManagerApi(
// create a new session for the userJHH
val (sessionToken, session) = this.sessionManager.newSessionData(person)
// create the response
// create the response with the session data
val response = NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK,
"text/plain",
"Session updated"
"application/json",
parser.toJson(session)
)
// set the session token in the cookies

View file

@ -1,6 +1,7 @@
package com.faraphel.tasks_valider.connectivity.task.session
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.google.gson.annotations.Expose
import kotlinx.serialization.Serializable
@ -9,5 +10,5 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class TaskSession(
val person: PersonEntity,
)
@Expose val person: PersonEntity,
)

View file

@ -18,6 +18,12 @@ interface PersonDao : BaseTaskDao<PersonEntity> {
@Query("SELECT * FROM ${PersonEntity.TABLE_NAME} WHERE id = :id")
fun getById(id: Long): PersonEntity?
/**
* Get the object from its card identifier
*/
@Query("SELECT * FROM ${PersonEntity.TABLE_NAME} WHERE card_id = :cardId")
fun getByCardId(cardId: String): PersonEntity?
@Query(
"SELECT * FROM ${PersonEntity.TABLE_NAME} " +
"WHERE id IN (" +

View file

@ -4,12 +4,13 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
@Entity(tableName = ClassEntity.TABLE_NAME)
data class ClassEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo("name") val name: String,
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) @Expose val id: Long = 0,
@ColumnInfo("name") @Expose val name: String,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "classes"

View file

@ -5,6 +5,7 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
import com.faraphel.tasks_valider.connectivity.task.session.TaskRole
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
import kotlinx.serialization.Serializable
import java.security.MessageDigest
import java.util.*
@ -12,12 +13,13 @@ import java.util.*
@Serializable
@Entity(tableName = PersonEntity.TABLE_NAME)
data class PersonEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo("first_name") val firstName: String,
@ColumnInfo("last_name") val lastName: String,
@ColumnInfo("card_id") val cardId: String? = null,
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) @Expose val id: Long = 0,
@ColumnInfo("first_name") @Expose val firstName: String,
@ColumnInfo("last_name") @Expose val lastName: String,
@ColumnInfo("card_id") @Expose val cardId: String? = null,
@ColumnInfo("password_hash") val passwordHash: String? = null,
@ColumnInfo("role") val role: TaskRole = TaskRole.STUDENT,
@ColumnInfo("role") @Expose val role: TaskRole = TaskRole.STUDENT,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "persons"

View file

@ -4,6 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
@Entity(
tableName = RelationClassPersonEntity.TABLE_NAME,
@ -27,8 +28,8 @@ import com.faraphel.tasks_valider.database.entities.base.BaseEntity
]
)
data class RelationClassPersonEntity (
@ColumnInfo("student_id", index = true) val studentId: Long,
@ColumnInfo("class_id", index = true) val classId: Long,
@ColumnInfo("student_id", index = true) @Expose val studentId: Long,
@ColumnInfo("class_id", index = true) @Expose val classId: Long,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "relation_class_person"

View file

@ -4,6 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
/**
@ -38,9 +39,9 @@ import com.faraphel.tasks_valider.database.entities.base.BaseEntity
]
)
data class RelationPersonSessionSubjectEntity (
@ColumnInfo("student_id", index = true) val studentId: Long,
@ColumnInfo("session_id", index = true) val sessionId: Long,
@ColumnInfo("subject_id", index = true) val subjectId: Long,
@ColumnInfo("student_id", index = true) @Expose val studentId: Long,
@ColumnInfo("session_id", index = true) @Expose val sessionId: Long,
@ColumnInfo("subject_id", index = true) @Expose val subjectId: Long,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "relation_person_session_subject"

View file

@ -5,6 +5,7 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
import java.time.Instant
@Entity(
@ -19,9 +20,9 @@ import java.time.Instant
]
)
data class SessionEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo("name") val name: String? = null,
@ColumnInfo("start") val start: Instant,
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) @Expose val id: Long = 0,
@ColumnInfo("name") @Expose val name: String? = null,
@ColumnInfo("start") @Expose val start: Instant,
@ColumnInfo("class_id", index = true) val classId: Long? = null,
) : BaseEntity() {

View file

@ -4,11 +4,12 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
@Entity(tableName = SubjectEntity.TABLE_NAME)
data class SubjectEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo("name") val name: String,
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) @Expose val id: Long = 0,
@ColumnInfo("name") @Expose val name: String,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "subjects"

View file

@ -5,6 +5,7 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
@Entity(
tableName = TaskEntity.TABLE_NAME,
@ -18,12 +19,12 @@ import com.faraphel.tasks_valider.database.entities.base.BaseEntity
]
)
data class TaskEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo("title") val title: String,
@ColumnInfo("description") val description: String? = null,
@ColumnInfo("order") val order: Int, ///< the order of the task in the list of the subject
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) @Expose val id: Long = 0,
@ColumnInfo("title") @Expose val title: String,
@ColumnInfo("description") @Expose val description: String? = null,
@ColumnInfo("order") @Expose val order: Int, ///< the order of the task in the list of the subject
@ColumnInfo("subject_id", index = true) val subjectId: Long,
@ColumnInfo("subject_id", index = true) @Expose val subjectId: Long,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "tasks"

View file

@ -4,6 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
import java.time.Instant
@Entity(
@ -35,11 +36,11 @@ import java.time.Instant
]
)
data class ValidationEntity (
@ColumnInfo("teacher_id", index = true) val teacherId: Long,
@ColumnInfo("student_id", index = true) val studentId: Long,
@ColumnInfo("task_id", index = true) val taskId: Long,
@ColumnInfo("teacher_id", index = true) @Expose val teacherId: Long,
@ColumnInfo("student_id", index = true) @Expose val studentId: Long,
@ColumnInfo("task_id", index = true) @Expose val taskId: Long,
@ColumnInfo("date") val date: Instant,
@ColumnInfo("date") @Expose val date: Instant,
) : BaseEntity() {
companion object {
const val TABLE_NAME = "validations"

View file

@ -3,4 +3,8 @@ package com.faraphel.tasks_valider.database.entities.error
class HttpException(
private val code: Int,
) : Exception("Http Exception: $code")
) : Exception("Http Exception: $code") {
fun getCode(): Int {
return code
}
}

View file

@ -74,46 +74,97 @@ fun populateTaskDatabaseTest(database: TaskDatabase) {
taskA1Id,
taskA2Id,
taskA3Id,
taskB1Id,
taskB2Id,
taskA4Id,
taskA5Id,
) = database.taskDao().insert(
// Subject A
TaskEntity(
title = "Commencer A",
description = "Description 1",
title = "Installation Debian",
description = "Installer la dernière version de Debian sur les Raspberry Pi.",
order = 1,
subjectId = subjectA.id,
),
TaskEntity(
title = "Continuer A",
description = "Description 2",
title = "Connection de LEDs",
description =
"Utiliser les broches GPIO du Raspberry Pi pour brancher vos LEDs.\n" +
"N'oublier pas les résistances !",
order = 2,
subjectId = subjectA.id
subjectId = subjectA.id,
),
TaskEntity(
title = "Finir A",
description = "Description 3",
title = "IDLE Python",
description = "Installer l'éditeur basique Python avec 'apt install idle-python3.11'.",
order = 3,
subjectId = subjectA.id
subjectId = subjectA.id,
),
TaskEntity(
title = "Commencer B",
description = "Description 1",
title = "Clignotement de LEDs",
description = "Faite clignoter les différentes LEDs.",
order = 4,
subjectId = subjectA.id,
),
TaskEntity(
title = "Feu Tricolore",
description = "Simuler un feu de circulation à l'aide de vos components.",
order = 5,
subjectId = subjectA.id,
),
)
val (
taskB1Id,
taskB2Id,
taskB3Id,
taskB4Id,
taskB5Id,
) = database.taskDao().insert(
// Subject B
TaskEntity(
title = "Préparation Arduino",
description = "Préparer votre Arduino et assurer vous qu'il soit reconnu par votre ordinateur.",
order = 1,
subjectId = subjectB.id,
),
TaskEntity(
title = "Finir B",
description = "Description 2",
title = "Connection de LEDs",
description =
"Utiliser les broches de votre Arduino pour brancher vos LEDs.\n" +
"N'oublier pas les résistances !",
order = 2,
subjectId = subjectB.id,
)
),
TaskEntity(
title = "IDE Arduino",
description = "Installer l'éditeur de code 'Arduino IDE'.",
order = 3,
subjectId = subjectB.id,
),
TaskEntity(
title = "Clignotement de LEDs",
description = "Faite clignoter les différentes LEDs.",
order = 4,
subjectId = subjectB.id,
),
TaskEntity(
title = "Feu Tricolore",
description = "Simuler un feu de circulation à l'aide de vos components.",
order = 5,
subjectId = subjectB.id,
),
)
val taskA1 = database.taskDao().getById(taskA1Id)!!
val taskA2 = database.taskDao().getById(taskA2Id)!!
val taskA3 = database.taskDao().getById(taskA3Id)!!
val taskA4 = database.taskDao().getById(taskA4Id)!!
val taskA5 = database.taskDao().getById(taskA5Id)!!
val taskB1 = database.taskDao().getById(taskB1Id)!!
val taskB2 = database.taskDao().getById(taskB2Id)!!
val taskB3 = database.taskDao().getById(taskB3Id)!!
val taskB4 = database.taskDao().getById(taskB4Id)!!
val taskB5 = database.taskDao().getById(taskB5Id)!!
}

View file

@ -1,40 +0,0 @@
package com.faraphel.tasks_valider.ui.screen.authentification
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.input.PasswordVisualTransformation
/**
* Authentification screen where the client can give his information
*/
@Composable
fun AuthentificationClientScreen() {
val cardId = remember { mutableStateOf("") }
val password = remember { mutableStateOf("") }
Column {
// card identifier
TextField(
value = cardId.value,
onValueChange = { text -> cardId.value = text },
)
// password
TextField(
value = password.value,
visualTransformation = PasswordVisualTransformation(),
onValueChange = { text -> password.value = text },
)
// button
Button(onClick = { /* do something */ }) {
Text("Submit")
}
}
}

View file

@ -0,0 +1,151 @@
package com.faraphel.tasks_valider.ui.screen.communication.authentication
import android.app.Activity
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.database.entities.error.HttpException
import com.faraphel.tasks_valider.ui.screen.scan.qr.ScanBarcodeScreen
import com.journeyapps.barcodescanner.BarcodeResult
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.net.ConnectException
/**
* Authentification screen where the client can give his information
*/
@Composable
fun AuthenticationClientScreen(activity: Activity, client: TaskClient, session: MutableState<TaskSession?>) {
val controller = rememberNavController()
val barcode = remember { mutableStateOf<BarcodeResult?>(null) }
NavHost(navController = controller, startDestination = "main") {
composable("main") {
AuthenticationClientContent(activity, controller, client, session, barcode)
}
composable("scan") {
if (barcode.value == null) ScanBarcodeScreen(activity, barcode)
else controller.navigate("main")
}
}
}
@Composable
fun AuthenticationClientContent(
activity: Activity,
controller: NavController,
client: TaskClient,
session: MutableState<TaskSession?>,
barcode: MutableState<BarcodeResult?>,
) {
val cardId = remember { mutableStateOf("") }
val password = remember { mutableStateOf("") }
// check if the barcode contain information about the card
var defaultCardId = ""
if (barcode.value != null) {
val studentUrl = barcode.value!!.text.toHttpUrl()
cardId.value = studentUrl.pathSegments[0]
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "Authentication",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// card identifier
Row {
// text
TextField(
value = cardId.value,
placeholder = { Text("Card Id") },
onValueChange = { text -> cardId.value = text },
)
// button to scan the card
Button(onClick = {
barcode.value = null
controller.navigate("scan")
}) {
Text("Scan")
}
}
// password
TextField(
value = password.value,
visualTransformation = PasswordVisualTransformation(),
placeholder = { Text("Password") },
onValueChange = { text -> password.value = text },
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// submit button
Button(onClick = {
Thread { authenticate(activity, client, cardId.value, password.value, session) }.start()
}) {
Text("Submit")
}
}
}
fun authenticate(
activity: Activity,
client: TaskClient,
cardId: String,
password: String,
session: MutableState<TaskSession?>
) {
try {
// try to get a session from the identifiers
session.value = client.session.newFromCardId(cardId, password)
} catch (exception: HttpException) {
// in case of error, show a message to the user
when (exception.getCode()) {
401 -> {
activity.runOnUiThread {
Toast.makeText(activity, "Invalid card id or password", Toast.LENGTH_SHORT).show()
}
}
else -> {
activity.runOnUiThread {
Toast.makeText(activity, "Unknown error: $exception", Toast.LENGTH_SHORT).show()
}
}
}
} catch (exception: ConnectException) {
activity.runOnUiThread {
Toast.makeText(activity, "Could not connect to the server: $exception", Toast.LENGTH_SHORT).show()
}
}
}

View file

@ -1,4 +1,4 @@
package com.faraphel.tasks_valider.ui.screen.authentification
package com.faraphel.tasks_valider.ui.screen.communication.authentication
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
@ -20,7 +20,7 @@ import com.faraphel.tasks_valider.database.entities.PersonEntity
* Authentification screen where the host can give his information
*/
@Composable
fun AuthentificationServerScreen(personEntity: MutableState<PersonEntity?>) {
fun AuthenticationServerScreen(personEntity: MutableState<PersonEntity?>) {
val firstName = remember { mutableStateOf("") }
val lastName = remember { mutableStateOf("") }

View file

@ -0,0 +1,109 @@
package com.faraphel.tasks_valider.ui.screen.communication.connection.internet.role
import android.app.Activity
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.ui.screen.communication.authentication.AuthenticationClientScreen
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.task.TaskSessionController
@Composable
fun CommunicationInternetClientScreen(activity: Activity) {
val controller = rememberNavController()
val client = remember { mutableStateOf<TaskClient?>(null) }
val session = remember { mutableStateOf<TaskSession?>(null) }
NavHost(controller, startDestination = "communication") {
composable("communication") {
CommunicationInternetClientContent(client)
if (client.value != null) controller.navigate("authentication")
}
composable("authentication") {
AuthenticationClientScreen(activity, client.value!!, session)
if (session.value != null) controller.navigate("session")
}
composable("session") {
// show the main screen
TaskSessionController(
activity,
client.value!!,
session.value!!.person,
)
}
}
}
@Composable
fun CommunicationInternetClientContent(client: MutableState<TaskClient?>) {
val serverAddress = remember { mutableStateOf(DEFAULT_SERVER_ADDRESS) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "Connection",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// server address
TextField(
value = serverAddress.value,
placeholder = { Text("server IP") },
onValueChange = { text -> serverAddress.value = text }
)
// server port
TextField(
value = serverPort.intValue.toString(),
placeholder = { Text("server port") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { text ->
val port = text.toInt()
if (port in RANGE_SERVER_PORT) {
serverPort.intValue = port
}
}
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// connect button
Button(onClick = {
// TODO(Faraphel): check if the server is reachable
client.value = TaskClient(serverAddress.value, serverPort.intValue)
}) {
Text("Connect")
}
}
}

View file

@ -1,9 +1,7 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet
package com.faraphel.tasks_valider.ui.screen.communication.connection.internet.role
import android.app.Activity
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
@ -27,7 +25,7 @@ import com.faraphel.tasks_valider.database.entities.ClassEntity
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.populateSubjectSessionPersonTest
import com.faraphel.tasks_valider.ui.screen.authentification.AuthentificationServerScreen
import com.faraphel.tasks_valider.ui.screen.communication.authentication.AuthenticationServerScreen
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.task.TaskSessionController
@ -37,7 +35,6 @@ import java.time.Instant
/**
* Screen for the host to configure the server
*/
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationInternetServerScreen(
activity: Activity,
@ -52,7 +49,7 @@ fun CommunicationInternetServerScreen(
NavHost(navController = controller, startDestination = "authentication") {
composable("authentication") {
// if the admin person is not created, prompt the user for the admin information
if (adminPersonEntityRaw.value == null) AuthentificationServerScreen(adminPersonEntityRaw)
if (adminPersonEntityRaw.value == null) AuthenticationServerScreen(adminPersonEntityRaw)
else controller.navigate("configuration")
}
composable("configuration") {
@ -76,7 +73,6 @@ fun CommunicationInternetServerScreen(
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationInternetServerContent(
database: TaskDatabase,
@ -183,12 +179,7 @@ fun CommunicationInternetServerContent(
val session = database.sessionDao().getById(sessionId)!!
// TODO(Faraphel): remove, this is a test function
Thread {
populateSubjectSessionPersonTest(database, session)
}.let { thread ->
thread.start()
thread.join()
}
populateSubjectSessionPersonTest(database, session)
// Create the server
Log.i("room-server", "creating the server")

View file

@ -1,8 +1,6 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet
package com.faraphel.tasks_valider.ui.screen.communication.connection.internet
import android.app.Activity
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
@ -16,9 +14,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.ui.screen.communication.connection.internet.role.CommunicationInternetClientScreen
import com.faraphel.tasks_valider.ui.screen.communication.connection.internet.role.CommunicationInternetServerScreen
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationInternetSelectScreen(activity: Activity, database: TaskDatabase) {
val controller = rememberNavController()

View file

@ -0,0 +1,97 @@
package com.faraphel.tasks_valider.ui.screen.communication.connection.wifiP2p.role
import android.app.Activity
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pDevice
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.*
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.connectivity.task.TaskClient
import com.faraphel.tasks_valider.connectivity.task.session.TaskSession
import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.communication.authentication.AuthenticationClientScreen
import com.faraphel.tasks_valider.ui.screen.communication.connection.internet.role.CommunicationInternetClientContent
import com.faraphel.tasks_valider.ui.screen.task.TaskSessionController
import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget
@Composable
fun CommunicationWifiP2pClientScreen(activity: Activity, bwdManager: BwdManager) {
val controller = rememberNavController()
val client = remember { mutableStateOf<TaskClient?>(null) }
val session = remember { mutableStateOf<TaskSession?>(null) }
NavHost(controller, startDestination = "communication") {
composable("communication") {
CommunicationWifiP2pClientContent(activity, client, bwdManager)
if (client.value != null) controller.navigate("authentication")
}
composable("authentication") {
AuthenticationClientScreen(activity, client.value!!, session)
if (session.value != null) controller.navigate("session")
}
composable("session") {
// show the main screen
TaskSessionController(
activity,
client.value!!,
session.value!!.person,
)
}
}
}
@Composable
fun CommunicationWifiP2pClientContent(
activity: Activity,
client: MutableState<TaskClient?>,
bwdManager: BwdManager,
) {
val selectedDevice = remember { mutableStateOf<WifiP2pDevice?>(null) }
// if the device is not selected
if (selectedDevice.value == null) {
Column {
WifiP2pDeviceListWidget(
peers = bwdManager.statePeers.value,
filter = { device: WifiP2pDevice -> device.isGroupOwner },
selectedDevice,
)
}
LaunchedEffect(true) {
// look for new peers
bwdManager.discoverPeers()
}
return
}
val config = WifiP2pConfig().apply {
deviceAddress = selectedDevice.value!!.deviceAddress
}
bwdManager.connect(config) {
bwdManager.requestConnectionInfo { connectionInfo ->
// if the group has not been formed correctly, the host did not approve the connection
if (!connectionInfo.groupFormed) {
selectedDevice.value = null
Toast.makeText(activity, "Require host approval.", Toast.LENGTH_LONG).show()
return@requestConnectionInfo
}
// create a connection to the server
// TODO(Faraphel): check if the server is reachable
client.value = TaskClient(
connectionInfo.groupOwnerAddress.hostAddress!!,
DEFAULT_SERVER_PORT
)
}
}
}

View file

@ -0,0 +1,187 @@
package com.faraphel.tasks_valider.ui.screen.communication.connection.wifiP2p.role
import android.app.Activity
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
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.database.entities.ClassEntity
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.populateSubjectSessionPersonTest
import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.communication.authentication.AuthenticationServerScreen
import com.faraphel.tasks_valider.ui.screen.task.TaskSessionController
import java.time.Instant
@Composable
fun CommunicationWifiP2pServerScreen(
activity: Activity,
database: TaskDatabase,
bwdManager: BwdManager
) {
val controller = rememberNavController()
val adminPersonEntityRaw = remember { mutableStateOf<PersonEntity?>(null) }
val adminPersonEntity = remember { mutableStateOf<PersonEntity?>(null) }
val client = remember { mutableStateOf<TaskClient?>(null) }
NavHost(navController = controller, startDestination = "authentication") {
composable("authentication") {
// if the admin person is not created, prompt the user for the admin information
if (adminPersonEntityRaw.value == null) AuthenticationServerScreen(adminPersonEntityRaw)
else controller.navigate("configuration")
}
composable("configuration") {
if (client.value == null)
CommunicationWifiP2pServerContent(
database,
bwdManager,
adminPersonEntityRaw,
adminPersonEntity,
client,
)
else controller.navigate("session")
}
composable("session") {
TaskSessionController(
activity,
client.value!!,
adminPersonEntity.value!!
)
}
}
// TODO(Faraphel): fix and get a user
// if the server is not created, prompt the user for the server configuration
// if (client.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, client)
// else, go to the base tasks screen
// else TaskSessionScreen(activity, client.value!!)
}
@Composable
fun CommunicationWifiP2pServerContent(
database: TaskDatabase,
bwdManager: BwdManager,
adminPersonEntityRaw: MutableState<PersonEntity?>,
adminPersonEntity: MutableState<PersonEntity?>,
client: MutableState<TaskClient?>
) {
val classes = remember { mutableStateOf<List<ClassEntity>?>(null) }
val selectedClass = remember { mutableStateOf<ClassEntity?>(null) }
val areClassesExpanded = remember { mutableStateOf(false) }
LaunchedEffect(true) {
// refresh the list of classes
Thread { refreshClasses(database, classes) }.start()
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "New Session",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// classes
Row(verticalAlignment = Alignment.CenterVertically) {
// description
Text(text = "Class", fontSize = 12.sp)
// separator
Spacer(modifier = Modifier.width(width = 12.dp))
// selector
Button(onClick = { areClassesExpanded.value = !areClassesExpanded.value }) {
// display the selected class, if selected
if (selectedClass.value != null) Text(text = selectedClass.value!!.name)
else Text(text = "<Not selected>")
}
// class selector
DropdownMenu(
expanded = areClassesExpanded.value,
onDismissRequest = { areClassesExpanded.value = false }
) {
// TODO(Faraphel): student lists should be loaded from the database or a file
classes.value?.forEach { class_ ->
DropdownMenuItem(
text = { Text(class_.name) },
onClick = {
selectedClass.value = class_
areClassesExpanded.value = false
}
)
}
}
}
// check if a class is selected
if (selectedClass.value != null)
// button to create the server
Button(onClick = {
Thread { // a thread is used for networking
// Insert the admin in the database and get its id
val adminPersonEntityId = database.personDao().insert(adminPersonEntityRaw.value!!)
adminPersonEntity.value = database.personDao().getById(adminPersonEntityId)!!
// Create a new session
// TODO(Faraphel): name
val sessionId = database.sessionDao().insert(
SessionEntity(
name="NOM",
start= Instant.now(),
classId=selectedClass.value!!.id,
)
)
val session = database.sessionDao().getById(sessionId)!!
// TODO(Faraphel): remove, this is a test function
populateSubjectSessionPersonTest(database, session)
// create a new Wi-Fi Direct group
bwdManager.recreateGroup {
// Create the server
Log.i("room-server", "creating the server")
val server = TaskServer(
DEFAULT_SERVER_PORT,
database,
session,
adminPersonEntity.value!!,
)
server.start()
// Get the client from the server
client.value = server.getAdminClient()
}
}.start()
}) {
Text("Create")
}
}
}
/**
* Refresh the list of classes
*/
fun refreshClasses(database: TaskDatabase, classes: MutableState<List<ClassEntity>?>) {
classes.value = database.classDao().getAll()
}

View file

@ -0,0 +1,62 @@
package com.faraphel.tasks_valider.ui.screen.communication.connection.wifiP2p
import android.annotation.SuppressLint
import android.app.Activity
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager.Companion.ALL_INTENT_FILTER
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.ui.screen.communication.connection.wifiP2p.role.CommunicationWifiP2pClientScreen
import com.faraphel.tasks_valider.ui.screen.communication.connection.wifiP2p.role.CommunicationWifiP2pServerScreen
@Composable
fun CommunicationWifiP2pScreen(activity: Activity, database: TaskDatabase) {
val controller = rememberNavController()
// create the Wi-Fi Direct manager
val bwdManager = remember { BwdManager.fromActivity(activity) }
NavHost(navController = controller, startDestination = "mode") {
composable("mode") { CommunicationWifiP2pSelectContent(controller) }
composable("client") { CommunicationWifiP2pClientScreen(activity, bwdManager) }
composable("server") { CommunicationWifiP2pServerScreen(activity, database, bwdManager) }
}
}
@Composable
fun CommunicationWifiP2pSelectContent(controller: NavController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "Role",
fontSize = 32.sp
)
// separator
Spacer(modifier = Modifier.height(24.dp))
// client mode
Button(onClick = { controller.navigate("client") }) { Text("Client") }
// server mode
Button(onClick = { controller.navigate("server") }) { Text("Server") }
}
}

View file

@ -1,67 +0,0 @@
package com.faraphel.tasks_valider.ui.screen.communication.internet
import android.app.Activity
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
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.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
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationInternetClientScreen(activity: Activity) {
val client = remember { mutableStateOf<TaskClient?>(null) }
// TODO(Faraphel): fix and get a user
// if (client.value == null) CommunicationInternetClientContent(client)
// else TaskSessionScreen(activity, client.value!!, user)
}
@Composable
fun CommunicationInternetClientContent(client: MutableState<TaskClient?>) {
val serverAddress = remember { mutableStateOf(DEFAULT_SERVER_ADDRESS) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
Column {
// server address
TextField(
value = serverAddress.value,
onValueChange = { text ->
serverAddress.value = text
}
)
// server port
TextField(
value = serverPort.intValue.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { text ->
val port = text.toInt()
if (port in RANGE_SERVER_PORT) {
serverPort.intValue = port
}
}
)
Button(onClick = {
// TODO(Faraphel): check if the server is reachable
client.value = TaskClient(serverAddress.value, serverPort.intValue)
}) {
Text("Connect")
}
}
}

View file

@ -1,9 +1,7 @@
package com.faraphel.tasks_valider.ui.screen.communication
import android.app.Activity
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@ -21,8 +19,8 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.ui.screen.communication.internet.CommunicationInternetSelectScreen
import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.CommunicationWifiP2pScreen
import com.faraphel.tasks_valider.ui.screen.communication.connection.internet.CommunicationInternetSelectScreen
import com.faraphel.tasks_valider.ui.screen.communication.connection.wifiP2p.CommunicationWifiP2pScreen
/**
@ -33,7 +31,6 @@ import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.CommunicationW
* @param activity: The activity that hosts the communication screen.
* @param database: the database.
*/
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CommunicationModeSelectionScreen(activity: Activity, database: TaskDatabase) {
val controller = rememberNavController()
@ -49,8 +46,7 @@ fun CommunicationModeSelectionScreen(activity: Activity, database: TaskDatabase)
CommunicationInternetSelectScreen(activity, database)
}
composable("wifi-p2p") {
val bwdManager = BwdManager.fromActivity(activity)
CommunicationWifiP2pScreen(activity, bwdManager)
CommunicationWifiP2pScreen(activity, database)
}
}
}
@ -92,10 +88,10 @@ fun CommunicationSelectContent(controller: NavController, activity: Activity) {
// if the WiFi-Direct is supported, navigate to the WiFi-Direct screen
if (isWifiP2pSupported) controller.navigate("wifi-p2p")
// if the WiFi-Direct is not supported, show a toast message
else Toast.makeText(activity, "WiFi-Direct is not supported on this device", Toast.LENGTH_SHORT).show()
else Toast.makeText(activity, "Wi-Fi Direct is not supported on this device", Toast.LENGTH_SHORT).show()
}
) {
Text("WiFi-Direct")
Text("Wi-Fi Direct")
}
}
}

View file

@ -1,57 +0,0 @@
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
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.bwd.BwdManager
import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget
@Composable
fun CommunicationWifiP2pClientScreen(activity: Activity, bwdManager: BwdManager) {
val selectedDevice = remember { mutableStateOf<WifiP2pDevice?>(null) }
val isConnected = remember { mutableStateOf(false) }
// if connected, show the task group screen
if (isConnected.value) {
// TaskGroupScreen(activity, null)
// TODO(Faraphel): finish the connection
return
}
// if the device is selected but not connected, try to connect
if (selectedDevice.value != null) {
// TODO(Faraphel): error handling
val config = WifiP2pConfig().apply {
deviceAddress = selectedDevice.value!!.deviceAddress
}
bwdManager.connect(config) {
isConnected.value = true
}
return
}
// display the list of devices
CommunicationWifiP2pClientContent(bwdManager, selectedDevice)
}
@Composable
fun CommunicationWifiP2pClientContent(
bwdManager: BwdManager,
selectedDevice: MutableState<WifiP2pDevice?>
) {
Column {
WifiP2pDeviceListWidget(
peers = bwdManager.statePeers.value,
filter = { device: WifiP2pDevice -> device.isGroupOwner },
selectedDevice,
)
}
}

View file

@ -1,37 +0,0 @@
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
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
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(activity: Activity, bwdManager: BwdManager) {
val controller = rememberNavController()
NavHost(navController = controller, startDestination = "mode") {
composable("mode") { CommunicationWifiP2pSelectContent(controller) }
composable("client") { CommunicationWifiP2pClientScreen(activity, bwdManager) }
composable("server") { CommunicationWifiP2pServerScreen(activity, bwdManager) }
}
}
@Composable
fun CommunicationWifiP2pSelectContent(controller: NavController) {
Column {
// client mode
Button(onClick = { controller.navigate("client") }) { Text("Client") }
// server mode
Button(onClick = { controller.navigate("server") }) { Text("Server") }
}
}

View file

@ -1,105 +0,0 @@
package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server
import android.app.Activity
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.bwd.BwdManager
import com.faraphel.tasks_valider.connectivity.task.TaskClient
@Composable
fun CommunicationWifiP2pServerScreen(activity: Activity, bwdManager: BwdManager) {
val client = remember { mutableStateOf<TaskClient?>(null) }
// TODO(Faraphel): fix and get a user
// if the server is not created, prompt the user for the server configuration
// if (client.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, client)
// else, go to the base tasks screen
// else TaskSessionScreen(activity, client.value!!)
}
@Composable
fun CommunicationWifiP2pServerContent(
activity: Activity,
bwdManager: BwdManager,
client: MutableState<TaskClient?>
) {
/*
val expandedStudentList = remember { mutableStateOf(false) }
val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) }
Column {
// student list
Button(onClick = { expandedStudentList.value = !expandedStudentList.value }) {
Text(text = "Select Students List")
}
DropdownMenu(
expanded = expandedStudentList.value,
onDismissRequest = { expandedStudentList.value = false }
) {
DropdownMenuItem(
text = { Text("ISRI") },
onClick = {}
)
DropdownMenuItem(
text = { Text("MIAGE") },
onClick = {}
)
// TODO(Faraphel): student lists should be loaded from the database or a file
}
// server port
TextField(
value = serverPort.intValue.toString(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { text ->
val port = text.toInt()
if (port in RANGE_SERVER_PORT) {
serverPort.intValue = port
}
}
)
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()
// Create the admin
// TODO: the admin should be created from the card reader
val adminPersonEntity = PersonEntity(
"admin",
"admin",
"123456789",
"admin",
)
// Insert the admin in the database
database.personDao().insert(adminPersonEntity)
bwfManager.recreateGroup {
// Create the server
val server = TaskServer(serverPort.intValue, database, adminPersonEntity)
server.start()
// Get the client from the server
client.value = server.getAdminClient()
}
}) {
Text("Create")
}
}
*/
}

View file

@ -66,7 +66,7 @@ fun quickValidation(
}
// requests all the persons
val allPersons = client.personApi.getAll()
val allPersons = client.entities.person.getAll()
// get the person with the matching card
val person = allPersons.firstOrNull { person -> person.cardId == cardId }
@ -79,19 +79,19 @@ fun quickValidation(
}
// requests all the relation persons - subjects
val allRelationsPersonSubject = client.relationPersonSessionSubjectApi.getAll()
val allRelationsPersonSubject = client.entities.relationPersonSessionSubject.getAll()
// get the corresponding relation
val relationPersonSubject = allRelationsPersonSubject.first { relation -> relation.studentId == person.id }
// requests all the tasks
val allTasks = client.taskApi.getAll()
val allTasks = client.entities.task.getAll()
// get the corresponding tasks
val tasks = allTasks
.filter { task -> task.subjectId == relationPersonSubject.subjectId }
.sortedBy { task -> task.order }
// requests all the validations
val allValidations = client.validationApi.getAll()
val allValidations = client.entities.validation.getAll()
// get the corresponding relation
val validations = allValidations.filter { validation -> validation.studentId == person.id }
@ -112,7 +112,7 @@ fun quickValidation(
}
// create a new validation on the server
client.validationApi.save(
client.entities.validation.save(
ValidationEntity(
teacherId=user.id,
studentId=person.id,

View file

@ -1,13 +1,8 @@
package com.faraphel.tasks_valider.ui.screen.task
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Environment
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
@ -125,7 +120,7 @@ fun refreshStudents(
) {
try {
// try to get all the persons in that session
students.value = client.personApi.getAll()
students.value = client.entities.person.getAll()
} catch (exception: Exception) {
// in case of error, show a message
return activity.runOnUiThread {
@ -141,12 +136,12 @@ fun exportToFile(
client: TaskClient,
) {
// get all the values to export
val allPersons = client.personApi.getAll()
val allPersons = client.entities.person.getAll()
val allStudents = allPersons.filter { student -> student.role == TaskRole.STUDENT }
val allRelationsStudentSessionSubject = client.relationPersonSessionSubjectApi.getAll()
val allSubjects = client.subjectApi.getAll()
val allTasks = client.taskApi.getAll()
val allValidations = client.validationApi.getAll()
val allRelationsStudentSessionSubject = client.entities.relationPersonSessionSubject.getAll()
val allSubjects = client.entities.subject.getAll()
val allTasks = client.entities.task.getAll()
val allValidations = client.entities.validation.getAll()
// for each student

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.faraphel.tasks_valider.connectivity.task.TaskClient
@ -43,7 +44,7 @@ fun TaskStudentScreen(
client,
student,
tasks,
validations
validations,
)
}.start()
@ -105,7 +106,7 @@ fun TaskStudentScreen(
client,
student,
tasks,
validations
validations,
)
}.start()
}
@ -125,7 +126,7 @@ fun refreshTasksValidations(
validations: MutableState<List<ValidationEntity>?>,
) {
// try to obtain the list of subject
val allRelationsPersonSessionSubject = client.relationPersonSessionSubjectApi.getAll()
val allRelationsPersonSessionSubject = client.entities.relationPersonSessionSubject.getAll()
// get the subject that the student is using
val relationPersonSessionSubject = allRelationsPersonSessionSubject.firstOrNull { relation ->
relation.studentId == student.id
@ -136,7 +137,7 @@ fun refreshTasksValidations(
return activity.runOnUiThread { Toast.makeText(activity, "No subject assigned", Toast.LENGTH_LONG).show() }
// try to obtain the list of tasks
val allTasks = client.taskApi.getAll()
val allTasks = client.entities.task.getAll()
// get the tasks that are linked to this subject
tasks.value = allTasks
@ -144,7 +145,7 @@ fun refreshTasksValidations(
.sortedBy { task -> task.order }
// try to obtain the list of validations
val allValidations = client.validationApi.getAll()
val allValidations = client.entities.validation.getAll()
// filter only the interesting validations
validations.value = allValidations.filter { validation ->
validation.studentId == student.id &&
@ -156,8 +157,8 @@ fun refreshTasksValidations(
fun updateValidation(client: TaskClient, checked: Boolean, validation: ValidationEntity) {
if (checked)
// if the validation is not set, create it
client.validationApi.save(validation)
client.entities.validation.save(validation)
else
// if the validation is set, delete it
client.validationApi.delete(validation)
client.entities.validation.delete(validation)
}

View file

@ -2,10 +2,14 @@ package com.faraphel.tasks_valider.ui.widgets.connectivity
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
@ -20,9 +24,24 @@ fun WifiP2pDeviceListWidget(
filter: ((WifiP2pDevice) -> Boolean)? = null,
deviceState: MutableState<WifiP2pDevice?>? = null,
) {
Text(text = "Devices (${peers?.deviceList?.size ?: 0})")
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// title
Text(
text = "Devices",
fontSize = 32.sp,
)
// found device count
Text(
text = "(${peers?.deviceList?.size ?: 0})",
fontSize = 24.sp,
)
// separator
Spacer(modifier = Modifier.height(24.dp))
Column {
// if there are peers to display
if (peers != null) {
// for every device in the list

View file

@ -8,4 +8,5 @@ import java.time.Instant
val parser: Gson = GsonBuilder()
.registerTypeAdapter(Instant::class.java, InstantConverter())
.excludeFieldsWithoutExposeAnnotation()
.create()