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 <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.Tasksvalider"> android:theme="@style/Theme.Tasksvalider"
android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View file

@ -1,6 +1,7 @@
package com.faraphel.tasks_valider.connectivity.bwd package com.faraphel.tasks_valider.connectivity.bwd
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
@ -41,15 +42,16 @@ class BwdManager(
* Create a new BwfManager from an activity. * Create a new BwfManager from an activity.
* @param activity The activity to create the manager from * @param activity The activity to create the manager from
*/ */
@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun fromActivity(activity: Activity): BwdManager { fun fromActivity(activity: Activity): BwdManager {
// check if the system support WiFi-Direct // 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") Log.e("wifi-p2p", "this device does not support the WiFi-Direct feature")
throw BwdNotSupportedException() throw BwdNotSupportedException()
} }
// TODO(Faraphel): more check on permissions // TODO(Faraphel): more check on permissions
if ( /* if (
activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
activity.checkSelfPermission(Manifest.permission.NEARBY_WIFI_DEVICES) != PackageManager.PERMISSION_GRANTED activity.checkSelfPermission(Manifest.permission.NEARBY_WIFI_DEVICES) != PackageManager.PERMISSION_GRANTED
) { ) {
@ -59,6 +61,7 @@ class BwdManager(
PERMISSION_ACCESS_FINE_LOCATION PERMISSION_ACCESS_FINE_LOCATION
) )
} }
*/
// get the WiFi-Direct manager // get the WiFi-Direct manager
val manager = activity.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager? val manager = activity.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager?
@ -66,9 +69,14 @@ class BwdManager(
// get the WiFi-Direct channel // get the WiFi-Direct channel
val channel = manager.initialize(activity, activity.mainLooper, null) 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 stateConnectionInfo = mutableStateOf<WifiP2pInfo?>(null)
val statePeers = mutableStateOf<WifiP2pDeviceList?>(null) val statePeers = mutableStateOf<WifiP2pDeviceList?>(null)
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
// ignore empty intent // ignore empty intent
if (intent == null) 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 The base have some issue, like an abusive usage of listener, error code and events that make using it
very impractical. very impractical.

View file

@ -1,5 +1,6 @@
package com.faraphel.tasks_valider.connectivity.task 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.TaskEntityHttpClient
import com.faraphel.tasks_valider.database.api.client.entities.* import com.faraphel.tasks_valider.database.api.client.entities.*
@ -17,13 +18,19 @@ class TaskClient(
) { ) {
private val httpClient = TaskEntityHttpClient(address, port, baseCookies) private val httpClient = TaskEntityHttpClient(address, port, baseCookies)
val clientApi = ClassClientApi(httpClient) // all the entities API
val personApi = PersonClientApi(httpClient) class Entities(httpClient: TaskEntityHttpClient) {
val sessionApi = SessionClientApi(httpClient) val client = ClassClientApi(httpClient)
val subjectApi = SubjectClientApi(httpClient) val person = PersonClientApi(httpClient)
val taskApi = TaskClientApi(httpClient) val session = SessionClientApi(httpClient)
val validationApi = ValidationClientApi(httpClient) val subject = SubjectClientApi(httpClient)
val task = TaskClientApi(httpClient)
val validation = ValidationClientApi(httpClient)
val relationClassPersonApi = RelationClassPersonClientApi(httpClient) val relationClassPerson = RelationClassPersonClientApi(httpClient)
val relationPersonSessionSubjectApi = RelationPersonSessionSubjectClientApi(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 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.connectivity.task.session.TaskSessionManager
import com.faraphel.tasks_valider.database.TaskDatabase import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.api.server.DatabaseApi import com.faraphel.tasks_valider.database.api.server.DatabaseApi
@ -24,7 +24,7 @@ class TaskServer(
) : NanoHTTPD(port) { ) : NanoHTTPD(port) {
private val sessionManager = TaskSessionManager(adminPersonEntity) ///< the session manager private val sessionManager = TaskSessionManager(adminPersonEntity) ///< the session manager
private val databaseApi = DatabaseApi(this.database, session) ///< the api of the database 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 * 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.TaskSession
import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager
import com.faraphel.tasks_valider.database.TaskDatabase import com.faraphel.tasks_valider.database.TaskDatabase
import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.faraphel.tasks_valider.utils.getBody
import com.faraphel.tasks_valider.utils.parser import com.faraphel.tasks_valider.utils.parser
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import fi.iki.elonen.NanoHTTPD import fi.iki.elonen.NanoHTTPD
@ -12,7 +14,7 @@ import fi.iki.elonen.NanoHTTPD
/** /**
* the HTTP API for the session manager * the HTTP API for the session manager
*/ */
class TaskSessionManagerApi( class TaskSessionManagerServerApi(
private val sessionManager: TaskSessionManager, private val sessionManager: TaskSessionManager,
private val database: TaskDatabase private val database: TaskDatabase
) { ) {
@ -72,25 +74,44 @@ class TaskSessionManagerApi(
NanoHTTPD.Method.POST -> { NanoHTTPD.Method.POST -> {
// get the user identifiers // get the user identifiers
val identifiers: Map<String, String> = parser.fromJson( val identifiers: Map<String, String> = parser.fromJson(
httpSession.inputStream.bufferedReader().readText(), httpSession.getBody(),
object : TypeToken<Map<String, String>>() {}.type object : TypeToken<Map<String, String>>() {}.type
) )
val person: PersonEntity
// check for the id // 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( return NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.BAD_REQUEST, NanoHTTPD.Response.Status.BAD_REQUEST,
"text/plain", "text/plain",
"Missing id" "Missing id or card_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 for the password // check for the password
if (!identifiers.contains("password")) if (!identifiers.contains("password"))
@ -100,14 +121,6 @@ class TaskSessionManagerApi(
"Missing password" "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 // check if the password is correct
if (!person.checkPassword(identifiers["password"]!!)) if (!person.checkPassword(identifiers["password"]!!))
return NanoHTTPD.newFixedLengthResponse( return NanoHTTPD.newFixedLengthResponse(
@ -119,11 +132,11 @@ class TaskSessionManagerApi(
// create a new session for the userJHH // create a new session for the userJHH
val (sessionToken, session) = this.sessionManager.newSessionData(person) val (sessionToken, session) = this.sessionManager.newSessionData(person)
// create the response // create the response with the session data
val response = NanoHTTPD.newFixedLengthResponse( val response = NanoHTTPD.newFixedLengthResponse(
NanoHTTPD.Response.Status.OK, NanoHTTPD.Response.Status.OK,
"text/plain", "application/json",
"Session updated" parser.toJson(session)
) )
// set the session token in the cookies // set the session token in the cookies

View file

@ -1,6 +1,7 @@
package com.faraphel.tasks_valider.connectivity.task.session package com.faraphel.tasks_valider.connectivity.task.session
import com.faraphel.tasks_valider.database.entities.PersonEntity import com.faraphel.tasks_valider.database.entities.PersonEntity
import com.google.gson.annotations.Expose
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -9,5 +10,5 @@ import kotlinx.serialization.Serializable
*/ */
@Serializable @Serializable
data class TaskSession( 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") @Query("SELECT * FROM ${PersonEntity.TABLE_NAME} WHERE id = :id")
fun getById(id: Long): PersonEntity? 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( @Query(
"SELECT * FROM ${PersonEntity.TABLE_NAME} " + "SELECT * FROM ${PersonEntity.TABLE_NAME} " +
"WHERE id IN (" + "WHERE id IN (" +

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity 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 ( data class RelationPersonSessionSubjectEntity (
@ColumnInfo("student_id", index = true) val studentId: Long, @ColumnInfo("student_id", index = true) @Expose val studentId: Long,
@ColumnInfo("session_id", index = true) val sessionId: Long, @ColumnInfo("session_id", index = true) @Expose val sessionId: Long,
@ColumnInfo("subject_id", index = true) val subjectId: Long, @ColumnInfo("subject_id", index = true) @Expose val subjectId: Long,
) : BaseEntity() { ) : BaseEntity() {
companion object { companion object {
const val TABLE_NAME = "relation_person_session_subject" 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.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.faraphel.tasks_valider.database.entities.base.BaseEntity import com.faraphel.tasks_valider.database.entities.base.BaseEntity
import com.google.gson.annotations.Expose
import java.time.Instant import java.time.Instant
@Entity( @Entity(
@ -19,9 +20,9 @@ import java.time.Instant
] ]
) )
data class SessionEntity ( data class SessionEntity (
@ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo("id") @PrimaryKey(autoGenerate = true) @Expose val id: Long = 0,
@ColumnInfo("name") val name: String? = null, @ColumnInfo("name") @Expose val name: String? = null,
@ColumnInfo("start") val start: Instant, @ColumnInfo("start") @Expose val start: Instant,
@ColumnInfo("class_id", index = true) val classId: Long? = null, @ColumnInfo("class_id", index = true) val classId: Long? = null,
) : BaseEntity() { ) : BaseEntity() {

View file

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

View file

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

View file

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

View file

@ -3,4 +3,8 @@ package com.faraphel.tasks_valider.database.entities.error
class HttpException( class HttpException(
private val code: Int, 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, taskA1Id,
taskA2Id, taskA2Id,
taskA3Id, taskA3Id,
taskB1Id, taskA4Id,
taskB2Id, taskA5Id,
) = database.taskDao().insert( ) = database.taskDao().insert(
// Subject A
TaskEntity( TaskEntity(
title = "Commencer A", title = "Installation Debian",
description = "Description 1", description = "Installer la dernière version de Debian sur les Raspberry Pi.",
order = 1, order = 1,
subjectId = subjectA.id, subjectId = subjectA.id,
), ),
TaskEntity( TaskEntity(
title = "Continuer A", title = "Connection de LEDs",
description = "Description 2", description =
"Utiliser les broches GPIO du Raspberry Pi pour brancher vos LEDs.\n" +
"N'oublier pas les résistances !",
order = 2, order = 2,
subjectId = subjectA.id subjectId = subjectA.id,
), ),
TaskEntity( TaskEntity(
title = "Finir A", title = "IDLE Python",
description = "Description 3", description = "Installer l'éditeur basique Python avec 'apt install idle-python3.11'.",
order = 3, order = 3,
subjectId = subjectA.id subjectId = subjectA.id,
), ),
TaskEntity( TaskEntity(
title = "Commencer B", title = "Clignotement de LEDs",
description = "Description 1", 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, order = 1,
subjectId = subjectB.id, subjectId = subjectB.id,
), ),
TaskEntity( TaskEntity(
title = "Finir B", title = "Connection de LEDs",
description = "Description 2", description =
"Utiliser les broches de votre Arduino pour brancher vos LEDs.\n" +
"N'oublier pas les résistances !",
order = 2, order = 2,
subjectId = subjectB.id, 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 taskA1 = database.taskDao().getById(taskA1Id)!!
val taskA2 = database.taskDao().getById(taskA2Id)!! val taskA2 = database.taskDao().getById(taskA2Id)!!
val taskA3 = database.taskDao().getById(taskA3Id)!! 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 taskB1 = database.taskDao().getById(taskB1Id)!!
val taskB2 = database.taskDao().getById(taskB2Id)!! 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.foundation.layout.*
import androidx.compose.material3.Button 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 * Authentification screen where the host can give his information
*/ */
@Composable @Composable
fun AuthentificationServerScreen(personEntity: MutableState<PersonEntity?>) { fun AuthenticationServerScreen(personEntity: MutableState<PersonEntity?>) {
val firstName = remember { mutableStateOf("") } val firstName = remember { mutableStateOf("") }
val lastName = 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.app.Activity
import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button 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.PersonEntity
import com.faraphel.tasks_valider.database.entities.SessionEntity import com.faraphel.tasks_valider.database.entities.SessionEntity
import com.faraphel.tasks_valider.database.populateSubjectSessionPersonTest 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.DEFAULT_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT
import com.faraphel.tasks_valider.ui.screen.task.TaskSessionController 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 * Screen for the host to configure the server
*/ */
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun CommunicationInternetServerScreen( fun CommunicationInternetServerScreen(
activity: Activity, activity: Activity,
@ -52,7 +49,7 @@ fun CommunicationInternetServerScreen(
NavHost(navController = controller, startDestination = "authentication") { NavHost(navController = controller, startDestination = "authentication") {
composable("authentication") { composable("authentication") {
// if the admin person is not created, prompt the user for the admin information // 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") else controller.navigate("configuration")
} }
composable("configuration") { composable("configuration") {
@ -76,7 +73,6 @@ fun CommunicationInternetServerScreen(
} }
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun CommunicationInternetServerContent( fun CommunicationInternetServerContent(
database: TaskDatabase, database: TaskDatabase,
@ -183,12 +179,7 @@ fun CommunicationInternetServerContent(
val session = database.sessionDao().getById(sessionId)!! val session = database.sessionDao().getById(sessionId)!!
// TODO(Faraphel): remove, this is a test function // TODO(Faraphel): remove, this is a test function
Thread { populateSubjectSessionPersonTest(database, session)
populateSubjectSessionPersonTest(database, session)
}.let { thread ->
thread.start()
thread.join()
}
// Create the server // Create the server
Log.i("room-server", "creating 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.app.Activity
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -16,9 +14,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.database.TaskDatabase 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 @Composable
fun CommunicationInternetSelectScreen(activity: Activity, database: TaskDatabase) { fun CommunicationInternetSelectScreen(activity: Activity, database: TaskDatabase) {
val controller = rememberNavController() 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 package com.faraphel.tasks_valider.ui.screen.communication
import android.app.Activity import android.app.Activity
import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@ -21,8 +19,8 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.faraphel.tasks_valider.connectivity.bwd.BwdManager import com.faraphel.tasks_valider.connectivity.bwd.BwdManager
import com.faraphel.tasks_valider.database.TaskDatabase 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.connection.internet.CommunicationInternetSelectScreen
import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.CommunicationWifiP2pScreen 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 activity: The activity that hosts the communication screen.
* @param database: the database. * @param database: the database.
*/ */
@RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun CommunicationModeSelectionScreen(activity: Activity, database: TaskDatabase) { fun CommunicationModeSelectionScreen(activity: Activity, database: TaskDatabase) {
val controller = rememberNavController() val controller = rememberNavController()
@ -49,8 +46,7 @@ fun CommunicationModeSelectionScreen(activity: Activity, database: TaskDatabase)
CommunicationInternetSelectScreen(activity, database) CommunicationInternetSelectScreen(activity, database)
} }
composable("wifi-p2p") { composable("wifi-p2p") {
val bwdManager = BwdManager.fromActivity(activity) CommunicationWifiP2pScreen(activity, database)
CommunicationWifiP2pScreen(activity, bwdManager)
} }
} }
} }
@ -92,10 +88,10 @@ fun CommunicationSelectContent(controller: NavController, activity: Activity) {
// if the WiFi-Direct is supported, navigate to the WiFi-Direct screen // if the WiFi-Direct is supported, navigate to the WiFi-Direct screen
if (isWifiP2pSupported) controller.navigate("wifi-p2p") if (isWifiP2pSupported) controller.navigate("wifi-p2p")
// if the WiFi-Direct is not supported, show a toast message // 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 // requests all the persons
val allPersons = client.personApi.getAll() val allPersons = client.entities.person.getAll()
// get the person with the matching card // get the person with the matching card
val person = allPersons.firstOrNull { person -> person.cardId == cardId } val person = allPersons.firstOrNull { person -> person.cardId == cardId }
@ -79,19 +79,19 @@ fun quickValidation(
} }
// requests all the relation persons - subjects // requests all the relation persons - subjects
val allRelationsPersonSubject = client.relationPersonSessionSubjectApi.getAll() val allRelationsPersonSubject = client.entities.relationPersonSessionSubject.getAll()
// get the corresponding relation // get the corresponding relation
val relationPersonSubject = allRelationsPersonSubject.first { relation -> relation.studentId == person.id } val relationPersonSubject = allRelationsPersonSubject.first { relation -> relation.studentId == person.id }
// requests all the tasks // requests all the tasks
val allTasks = client.taskApi.getAll() val allTasks = client.entities.task.getAll()
// get the corresponding tasks // get the corresponding tasks
val tasks = allTasks val tasks = allTasks
.filter { task -> task.subjectId == relationPersonSubject.subjectId } .filter { task -> task.subjectId == relationPersonSubject.subjectId }
.sortedBy { task -> task.order } .sortedBy { task -> task.order }
// requests all the validations // requests all the validations
val allValidations = client.validationApi.getAll() val allValidations = client.entities.validation.getAll()
// get the corresponding relation // get the corresponding relation
val validations = allValidations.filter { validation -> validation.studentId == person.id } val validations = allValidations.filter { validation -> validation.studentId == person.id }
@ -112,7 +112,7 @@ fun quickValidation(
} }
// create a new validation on the server // create a new validation on the server
client.validationApi.save( client.entities.validation.save(
ValidationEntity( ValidationEntity(
teacherId=user.id, teacherId=user.id,
studentId=person.id, studentId=person.id,

View file

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

View file

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

View file

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