diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2568df0..802e9a1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,7 +11,7 @@ android { defaultConfig { applicationId = "com.faraphel.tasks_valider" - minSdk = 24 + minSdk = 26 targetSdk = 34 versionCode = 1 versionName = "1.0" @@ -52,8 +52,8 @@ android { } dependencies { - implementation("androidx.core:core-ktx:1.13.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1") implementation("androidx.activity:activity-compose:1.9.0") implementation(platform("androidx.compose:compose-bom:2023.08.00")) implementation("androidx.compose.ui:ui") @@ -66,6 +66,8 @@ dependencies { implementation("org.nanohttpd:nanohttpd:2.3.1") implementation("com.google.code.gson:gson:2.10.1") implementation("com.squareup.okhttp3:okhttp-android:5.0.0-alpha.14") + implementation("com.journeyapps:zxing-android-embedded:4.3.0") + implementation("com.google.zxing:core:3.5.3") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -74,4 +76,5 @@ dependencies { debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") ksp("androidx.room:room-compiler:2.6.1") + implementation(kotlin("reflect")) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 051a946..32f3c79 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,12 +5,12 @@ - + @@ -26,6 +26,10 @@ android:usesPermissionFlags="neverForLocation" tools:targetApi="s" /> + + + + @@ -47,9 +51,12 @@ android:theme="@style/Theme.Tasksvalider"> - + + + + diff --git a/app/src/main/java/com/faraphel/tasks_valider/MainActivity.kt b/app/src/main/java/com/faraphel/tasks_valider/MainActivity.kt index b35c547..3b00408 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/MainActivity.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/MainActivity.kt @@ -6,24 +6,41 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.RequiresApi -import com.faraphel.tasks_valider.connectivity.bwf.BwfManager +import androidx.room.Room +import com.faraphel.tasks_valider.connectivity.bwd.BwdManager import com.faraphel.tasks_valider.database.TaskDatabase -import com.faraphel.tasks_valider.ui.screen.communication.CommunicationScreen +import com.faraphel.tasks_valider.database.populateTaskDatabaseTest +import com.faraphel.tasks_valider.ui.screen.communication.CommunicationModeSelectionScreen class MainActivity : ComponentActivity() { - private var bwfManager: BwfManager? = null ///< the WiFi-Direct helper + private var bwdManager: BwdManager? = null ///< the WiFi-Direct helper + private lateinit var database: TaskDatabase ///< the database manager - companion object { - private lateinit var database: TaskDatabase ///< the database manager - } - - @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Reset the database | TODO(Faraphel): only for testing purpose + this.deleteDatabase("local") + + // Create the database + this.database = Room.databaseBuilder( + this, + TaskDatabase::class.java, + "local" + ).build() + + // Populate the database with test data + // TODO(Faraphel): remove test data + Thread { + populateTaskDatabaseTest(database) + }.let { thread -> + thread.start() + thread.join() + } + this.setContent { - CommunicationScreen(this) + CommunicationModeSelectionScreen(this, database) } } @@ -32,13 +49,13 @@ class MainActivity : ComponentActivity() { super.onResume() // enable the WiFi-Direct events - this.registerReceiver(this.bwfManager, BwfManager.ALL_INTENT_FILTER) + this.registerReceiver(this.bwdManager, BwdManager.ALL_INTENT_FILTER) } override fun onPause() { super.onPause() // disable the WiFi-Direct events - this.unregisterReceiver(this.bwfManager) + this.unregisterReceiver(this.bwdManager) } } diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/BwfManager.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/BwdManager.kt similarity index 91% rename from app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/BwfManager.kt rename to app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/BwdManager.kt index 31f6681..e1d1994 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/BwfManager.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/BwdManager.kt @@ -1,4 +1,4 @@ -package com.faraphel.tasks_valider.connectivity.bwf +package com.faraphel.tasks_valider.connectivity.bwd import android.Manifest import android.app.Activity @@ -10,7 +10,7 @@ import android.content.pm.PackageManager import android.net.wifi.p2p.* import android.util.Log import androidx.compose.runtime.mutableStateOf -import com.faraphel.tasks_valider.connectivity.bwf.error.* +import com.faraphel.tasks_valider.connectivity.bwd.error.* /** @@ -22,7 +22,7 @@ import com.faraphel.tasks_valider.connectivity.bwf.error.* * @param manager The WiFi-Direct manager * @param channel The WiFi-Direct channel */ -class BwfManager( +class BwdManager( private var manager: WifiP2pManager, private var channel: WifiP2pManager.Channel, ) : BroadcastReceiver() { @@ -41,11 +41,11 @@ class BwfManager( * Create a new BwfManager from an activity. * @param activity The activity to create the manager from */ - fun fromActivity(activity: Activity): BwfManager { + fun fromActivity(activity: Activity): BwdManager { // check if the system support WiFi-Direct if (this.isSupported(activity)) { Log.e("wifi-p2p", "this device does not support the WiFi-Direct feature") - throw BwfNotSupportedException() + throw BwdNotSupportedException() } // TODO(Faraphel): more check on permissions @@ -62,11 +62,11 @@ class BwfManager( // get the WiFi-Direct manager val manager = activity.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager? - ?: throw BwfPermissionException() + ?: throw BwdPermissionException() // get the WiFi-Direct channel val channel = manager.initialize(activity, activity.mainLooper, null) - return BwfManager(manager, channel) + return BwdManager(manager, channel) // NOTE(Faraphel): the broadcast receiver should be registered in the activity onResume } @@ -83,7 +83,7 @@ class BwfManager( fun connect(config: WifiP2pConfig, callback: () -> Unit = {}) = this.manager.connect(this.channel, config, object : WifiP2pManager.ActionListener { override fun onSuccess() { callback() } - override fun onFailure(reason: Int) = throw BwfConnectException(reason) + override fun onFailure(reason: Int) = throw BwdConnectException(reason) }) /** @@ -105,7 +105,7 @@ class BwfManager( fun discoverPeers(callback: () -> Unit = {}) = this.manager.discoverPeers(this.channel, object : WifiP2pManager.ActionListener { override fun onSuccess() { callback() } - override fun onFailure(reason: Int) = throw BwfDiscoverException(reason) + override fun onFailure(reason: Int) = throw BwdDiscoverException(reason) }) /** @@ -133,7 +133,7 @@ class BwfManager( fun createGroup(callback: () -> Unit = {}) = this.manager.createGroup(this.channel, object : WifiP2pManager.ActionListener { override fun onSuccess() { callback() } - override fun onFailure(reason: Int) = throw BwfCreateGroupException(reason) + override fun onFailure(reason: Int) = throw BwdCreateGroupException(reason) }) /** @@ -143,7 +143,7 @@ class BwfManager( fun removeGroup(callback: () -> Unit = {}) = this.manager.removeGroup(this.channel, object : WifiP2pManager.ActionListener { override fun onSuccess() { callback() } - override fun onFailure(reason: Int) = throw BwfRemoveGroupException(reason) + override fun onFailure(reason: Int) = throw BwdRemoveGroupException(reason) }) /** @@ -161,7 +161,7 @@ class BwfManager( this.requestGroupInfo { group -> // if a group exist, quit it if (group != null) - this.removeGroup { this@BwfManager.createGroup(callback) } + this.removeGroup { this@BwdManager.createGroup(callback) } else // create the group this.createGroup(callback) diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/README.md b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/README.md similarity index 100% rename from app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/README.md rename to app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/README.md diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdConnectException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdConnectException.kt new file mode 100644 index 0000000..4808a4b --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdConnectException.kt @@ -0,0 +1,5 @@ +package com.faraphel.tasks_valider.connectivity.bwd.error + +class BwdConnectException( + reason: Int +) : BwdException("Cannot connect to the peer. Reason: $reason") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdCreateGroupException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdCreateGroupException.kt new file mode 100644 index 0000000..e336cc0 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdCreateGroupException.kt @@ -0,0 +1,5 @@ +package com.faraphel.tasks_valider.connectivity.bwd.error + +class BwdCreateGroupException ( + reason: Int +) : BwdException("Could not create the group : $reason") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdDiscoverException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdDiscoverException.kt new file mode 100644 index 0000000..ab6fece --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdDiscoverException.kt @@ -0,0 +1,5 @@ +package com.faraphel.tasks_valider.connectivity.bwd.error + +class BwdDiscoverException( + reason: Int +) : BwdException("Could not discover peers : $reason") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdException.kt similarity index 61% rename from app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfException.kt rename to app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdException.kt index 7fd8810..3d5e99b 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfException.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdException.kt @@ -1,9 +1,9 @@ -package com.faraphel.tasks_valider.connectivity.bwf.error +package com.faraphel.tasks_valider.connectivity.bwd.error /** * Base Exception for everything concerning the WifiP2pHelper class */ -open class BwfException( +open class BwdException( override val message: String? ) : Exception(message) diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdInvalidActionException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdInvalidActionException.kt new file mode 100644 index 0000000..ce50bd8 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdInvalidActionException.kt @@ -0,0 +1,5 @@ +package com.faraphel.tasks_valider.connectivity.bwd.error + +class BwdInvalidActionException( + action: String +) : BwdException("This WiFi-Direct action is not supported : $action") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdNotSupportedException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdNotSupportedException.kt new file mode 100644 index 0000000..9a2b46b --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdNotSupportedException.kt @@ -0,0 +1,4 @@ +package com.faraphel.tasks_valider.connectivity.bwd.error + +class BwdNotSupportedException : + BwdException("WiFi-Direct is not supported on this device.") \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdPermissionException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdPermissionException.kt new file mode 100644 index 0000000..f30bd58 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdPermissionException.kt @@ -0,0 +1,4 @@ +package com.faraphel.tasks_valider.connectivity.bwd.error + +class BwdPermissionException : + BwdException("WiFi-Direct requires permissions to work properly. Please grant the permissions.") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdRemoveGroupException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdRemoveGroupException.kt new file mode 100644 index 0000000..e5456e7 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwd/error/BwdRemoveGroupException.kt @@ -0,0 +1,5 @@ +package com.faraphel.tasks_valider.connectivity.bwd.error + +class BwdRemoveGroupException ( + reason: Int +) : BwdException("Could not remove the group : $reason") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfConnectException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfConnectException.kt deleted file mode 100644 index a581c09..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfConnectException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.faraphel.tasks_valider.connectivity.bwf.error - -class BwfConnectException( - reason: Int -) : BwfException("Cannot connect to the peer. Reason: $reason") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfCreateGroupException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfCreateGroupException.kt deleted file mode 100644 index 0ce1169..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfCreateGroupException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.faraphel.tasks_valider.connectivity.bwf.error - -class BwfCreateGroupException ( - reason: Int -) : BwfException("Could not create the group : $reason") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfDiscoverException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfDiscoverException.kt deleted file mode 100644 index a4e818a..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfDiscoverException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.faraphel.tasks_valider.connectivity.bwf.error - -class BwfDiscoverException( - reason: Int -) : BwfException("Could not discover peers : $reason") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfInvalidActionException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfInvalidActionException.kt deleted file mode 100644 index 0b690a3..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfInvalidActionException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.faraphel.tasks_valider.connectivity.bwf.error - -class BwfInvalidActionException( - action: String -) : BwfException("This WiFi-Direct action is not supported : $action") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfNotSupportedException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfNotSupportedException.kt deleted file mode 100644 index 3585496..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfNotSupportedException.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.faraphel.tasks_valider.connectivity.bwf.error - -class BwfNotSupportedException : - BwfException("WiFi-Direct is not supported on this device.") \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfPermissionException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfPermissionException.kt deleted file mode 100644 index 81c85ee..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfPermissionException.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.faraphel.tasks_valider.connectivity.bwf.error - -class BwfPermissionException : - BwfException("WiFi-Direct requires permissions to work properly. Please grant the permissions.") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfRemoveGroupException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfRemoveGroupException.kt deleted file mode 100644 index 92d0789..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfRemoveGroupException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.faraphel.tasks_valider.connectivity.bwf.error - -class BwfRemoveGroupException ( - reason: Int -) : BwfException("Could not remove the group : $reason") diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskClient.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskClient.kt index b1917e1..e4b7bd5 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskClient.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskClient.kt @@ -1,13 +1,11 @@ package com.faraphel.tasks_valider.connectivity.task -import okhttp3.HttpUrl -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.RequestBody.Companion.toRequestBody +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.* /** - * A client to handle the room connection. + * A client to handle the room connection and access the API * @param address the address of the server * @param port the port of the server * @param baseCookies list of cookies to use (optional) @@ -17,84 +15,15 @@ class TaskClient( private val port: Int, private val baseCookies: List = listOf() ) { - private val baseUrl = "http://$address:$port" - private val client = OkHttpClient().newBuilder().cookieJar( - // TODO(Faraphel): should be moved into another object - object : okhttp3.CookieJar { - private val cookies = baseCookies.toMutableList() ///< list of cookies + private val httpClient = TaskEntityHttpClient(address, port, baseCookies) - override fun loadForRequest(url: HttpUrl): List { - return this.cookies - } - override fun saveFromResponse(url: HttpUrl, cookies: List) { - this.cookies.addAll(cookies) - } - } - ).build() + val clientApi = ClassClientApi(httpClient) + val personApi = PersonClientApi(httpClient) + val sessionApi = SessionClientApi(httpClient) + val subjectApi = SubjectClientApi(httpClient) + val taskApi = TaskClientApi(httpClient) + val validationApi = ValidationClientApi(httpClient) - // TODO(Faraphel): automatically convert content to the correct type ? - - /** - * Return a basic request to the server - * @param endpoint the endpoint of the server - */ - private fun baseRequestBuilder(endpoint: String): okhttp3.Request.Builder = - okhttp3.Request.Builder().url("$baseUrl/$endpoint") - - /** - * Run a HEAD request - * @param endpoint the endpoint of the server - */ - fun head(endpoint: String): okhttp3.Request = - this.baseRequestBuilder(endpoint).head().build() - - /** - * Run a GET request - * @param endpoint the endpoint of the server - */ - fun get(endpoint: String): okhttp3.Response = - this.client.newCall( - this.baseRequestBuilder(endpoint) - .get() - .build() - ).execute() - - /** - * Run a POST request - * @param endpoint the endpoint of the server - * @param content the content of the request - * @param type the type of the content - */ - fun post(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response = - this.client.newCall( - this.baseRequestBuilder(endpoint) - .post(content.toRequestBody(type.toMediaType())) - .build() - ).execute() - - /** - * Run a PATCH request - * @param endpoint the endpoint of the server - * @param content the content of the request - * @param type the type of the content - */ - fun patch(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response = - this.client.newCall( - this.baseRequestBuilder(endpoint) - .patch(content.toRequestBody(type.toMediaType())) - .build() - ).execute() - - /** - * Run a DELETE request - * @param endpoint the endpoint of the server - * @param content the content of the request - * @param type the type of the content - */ - fun delete(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response = - this.client.newCall( - this.baseRequestBuilder(endpoint) - .delete(content.toRequestBody(type.toMediaType())) - .build() - ).execute() + val relationClassPersonApi = RelationClassPersonClientApi(httpClient) + val relationPersonSessionSubjectApi = RelationPersonSessionSubjectClientApi(httpClient) } \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskServer.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskServer.kt index ba2151a..a4c1893 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskServer.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskServer.kt @@ -1,45 +1,51 @@ package com.faraphel.tasks_valider.connectivity.task import com.faraphel.tasks_valider.connectivity.task.api.TaskSessionManagerApi -import com.faraphel.tasks_valider.connectivity.task.session.TaskRole -import com.faraphel.tasks_valider.connectivity.task.session.TaskSession import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager import com.faraphel.tasks_valider.database.TaskDatabase -import com.faraphel.tasks_valider.database.api.TaskDatabaseApi +import com.faraphel.tasks_valider.database.api.server.DatabaseApi +import com.faraphel.tasks_valider.database.entities.PersonEntity +import com.faraphel.tasks_valider.database.entities.SessionEntity import fi.iki.elonen.NanoHTTPD /** * A server to handle the task API to allow clients to interact with the database. - * @param port the port of the server - * @param database the database to interact with + * @param port the port of the server. + * @param database the database to interact with. + * @param session the current session. + * @param adminPersonEntity the person that represent the host of the session. */ class TaskServer( private val port: Int, - private val database: TaskDatabase + private val database: TaskDatabase, + private val session: SessionEntity, + private val adminPersonEntity: PersonEntity, ) : NanoHTTPD(port) { - companion object { - private val TASK_SESSION_ADMIN = TaskSession( ///< the admin default session - role = TaskRole.ADMIN - ) + 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 + + /** + * Get the admin person entity + * @return the admin person entity + */ + fun getAdminPersonEntity(): PersonEntity { + return this.sessionManager.getAdminPersonEntity() } - private val sessionManager = TaskSessionManager() ///< the session manager - private val adminSessionId = this.sessionManager.newSessionData(TASK_SESSION_ADMIN) ///< default admin session id - - private val sessionManagerApi = TaskSessionManagerApi(this.sessionManager) ///< the api of the session manager - private val databaseApi = TaskDatabaseApi(this.database) ///< the api of the database - /** * Return a new client that can be used by the admin + * @return the client */ - fun getClientAdmin(): TaskClient { + fun getAdminClient(): TaskClient { // create the session cookie for the admin val cookieSession = okhttp3.Cookie.Builder() .domain("localhost") - .name("sessionId") - .value(adminSessionId) - .build() + .name("sessionToken") + .value(this.sessionManager.adminSessionToken) + .build() + // create a new client return TaskClient( "localhost", @@ -54,7 +60,8 @@ class TaskServer( */ override fun serve(httpSession: IHTTPSession): Response { // get the session data of the client - val taskSession = this.sessionManager.getOrCreateSessionData(httpSession) + val taskSessionData = this.sessionManager.getSessionData(httpSession) + val taskSession = taskSessionData?.second // parse the url val uri: String = httpSession.uri.trim('/') @@ -69,11 +76,11 @@ class TaskServer( ) // get the response from the correct part of the application - val response = when (requestType) { + return when (requestType) { // session requests "sessions" -> this.sessionManagerApi.handleRequest(taskSession, httpSession, path) // entities requests - "entities" -> return this.databaseApi.handleRequest(taskSession, httpSession, path) + "entities" -> this.databaseApi.handleRequest(taskSession, httpSession, path) // invalid requests else -> newFixedLengthResponse( @@ -82,9 +89,6 @@ class TaskServer( "Unknown request type" ) } - - // wrap additional information in the response - return this.sessionManager.responseSetSessionData(response, httpSession.cookies) } /** diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/api/TaskSessionManagerApi.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/api/TaskSessionManagerApi.kt index 305e3f3..6d6f055 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/api/TaskSessionManagerApi.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/api/TaskSessionManagerApi.kt @@ -1,10 +1,10 @@ package com.faraphel.tasks_valider.connectivity.task.api import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission -import com.faraphel.tasks_valider.connectivity.task.session.TaskRole import com.faraphel.tasks_valider.connectivity.task.session.TaskSession import com.faraphel.tasks_valider.connectivity.task.session.TaskSessionManager -import com.google.gson.Gson +import com.faraphel.tasks_valider.database.TaskDatabase +import com.faraphel.tasks_valider.utils.parser import com.google.gson.reflect.TypeToken import fi.iki.elonen.NanoHTTPD @@ -12,9 +12,10 @@ import fi.iki.elonen.NanoHTTPD /** * the HTTP API for the session manager */ -class TaskSessionManagerApi(private val sessionManager: TaskSessionManager) { - private val jsonParser = Gson() ///< the json parser - +class TaskSessionManagerApi( + private val sessionManager: TaskSessionManager, + private val database: TaskDatabase +) { /** * Handle a HTTP Api request * @param taskSession the data of the client session @@ -22,109 +23,118 @@ class TaskSessionManagerApi(private val sessionManager: TaskSessionManager) { * @param path the path of the request */ fun handleRequest( - taskSession: TaskSession, + taskSession: TaskSession?, httpSession: NanoHTTPD.IHTTPSession, path: MutableList, ): NanoHTTPD.Response { - // get the target session id - val targetSessionId = path.removeFirstOrNull() + val action = path.removeFirstOrNull() - return if (targetSessionId == null) { - // no specific session targeted - this.handleRequestGeneric(taskSession, httpSession) - } else { - // a specific session is targeted - this.handleRequestSpecific(taskSession, httpSession, targetSessionId) + return when (action) { + "self" -> this.handleRequestSelf(taskSession, httpSession, path) + "all" -> this.handleRequestAll(taskSession, httpSession, path) + else -> + NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.BAD_REQUEST, + "text/plain", + "Unknown action" + ) } } /** - * Handle a request with no specific session targeted + * Handle an HTTP Api request about the user own session */ - private fun handleRequestGeneric( - taskSession: TaskSession, + fun handleRequestSelf( + taskSession: TaskSession?, httpSession: NanoHTTPD.IHTTPSession, + path: MutableList, ): NanoHTTPD.Response { when (httpSession.method) { - // get all the session data + // get the session data NanoHTTPD.Method.GET -> { - // check the permission of the session - if (taskSession.role.permissions.contains(TaskPermission.READ)) + // check if the session is valid + if (taskSession == null) return NanoHTTPD.newFixedLengthResponse( - NanoHTTPD.Response.Status.FORBIDDEN, + NanoHTTPD.Response.Status.UNAUTHORIZED, "text/plain", - "Forbidden" + "No session" ) // return the session data return NanoHTTPD.newFixedLengthResponse( NanoHTTPD.Response.Status.OK, "application/json", - jsonParser.toJson(taskSession) + parser.toJson(taskSession) ) } - // other action are limited - else -> { - return NanoHTTPD.newFixedLengthResponse( - NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, - "text/plain", - "Unknown method" - ) - } - } - } - private fun handleRequestSpecific( - taskSession: TaskSession, - httpSession: NanoHTTPD.IHTTPSession, - targetSessionId: String, - ): NanoHTTPD.Response { - when (httpSession.method) { - // change a specific client session data + // connect the user to the session NanoHTTPD.Method.POST -> { - // check the permission of the session - if (taskSession.role.permissions.contains(TaskPermission.ADMIN)) + // get the user identifiers + val identifiers: Map = parser.fromJson( + httpSession.inputStream.bufferedReader().readText(), + object : TypeToken>() {}.type + ) + + // check for the id + if (!identifiers.contains("id")) return NanoHTTPD.newFixedLengthResponse( - NanoHTTPD.Response.Status.FORBIDDEN, + NanoHTTPD.Response.Status.BAD_REQUEST, "text/plain", - "You are not allowed to update a session" + "Missing id" ) - // parse the content of the request - val targetSession = jsonParser.fromJson( - httpSession.inputStream.bufferedReader().readText(), - TaskSession::class.java - ) + // 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" + ) - // update the session - this.sessionManager.setSessionData(targetSessionId, targetSession) + // check for the password + if (!identifiers.contains("password")) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.BAD_REQUEST, + "text/plain", + "Missing password" + ) - // success message - return NanoHTTPD.newFixedLengthResponse( + // 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( + NanoHTTPD.Response.Status.UNAUTHORIZED, + "text/plain", + "Invalid password" + ) + + // create a new session for the userJHH + val (sessionToken, session) = this.sessionManager.newSessionData(person) + + // create the response + val response = NanoHTTPD.newFixedLengthResponse( NanoHTTPD.Response.Status.OK, "text/plain", "Session updated" ) - } - // delete the session - NanoHTTPD.Method.DELETE -> { - // check the permission of the session - if (taskSession.role.permissions.contains(TaskPermission.ADMIN)) - return NanoHTTPD.newFixedLengthResponse( - NanoHTTPD.Response.Status.FORBIDDEN, - "text/plain", - "You are not allowed to delete a session" - ) - // delete the target session - this.sessionManager.deleteSessionData(targetSessionId) - - // success message - return NanoHTTPD.newFixedLengthResponse( - NanoHTTPD.Response.Status.OK, - "text/plain", - "Session deleted" + // set the session token in the cookies + this.sessionManager.responseSetSessionData( + response, + httpSession.cookies, + sessionToken ) + + // return the response + return response } // ignore other methods else -> { @@ -136,4 +146,86 @@ class TaskSessionManagerApi(private val sessionManager: TaskSessionManager) { } } } -} \ No newline at end of file + + /** + * Handle an HTTP Api request about all the sessions + */ + private fun handleRequestAll( + taskSession: TaskSession?, + httpSession: NanoHTTPD.IHTTPSession, + path: MutableList, + ): NanoHTTPD.Response { + // check if the session is valid + if (taskSession == null) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.UNAUTHORIZED, + "text/plain", + "No session" + ) + + // check the permission of the session + if (taskSession.person.role.permissions.contains(TaskPermission.ADMIN)) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.FORBIDDEN, + "text/plain", + "You are not allowed to update a session" + ) + + when (httpSession.method) { + // change a specific client session data + NanoHTTPD.Method.POST -> { + // parse the content of the request + val targetSession = parser.fromJson( + httpSession.inputStream.bufferedReader().readText(), + TaskSession::class.java + ) + + val targetSessionToken = path.removeFirstOrNull() + ?: return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.BAD_REQUEST, + "text/plain", + "Missing session token" + ) + + // update the session + this.sessionManager.updateSessionData(targetSessionToken, targetSession) + + // success message + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.OK, + "text/plain", + "Session updated" + ) + } + + // delete the session + NanoHTTPD.Method.DELETE -> { + val targetSessionToken = path.removeFirstOrNull() + ?: return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.BAD_REQUEST, + "text/plain", + "Missing session token" + ) + + // delete the target session + this.sessionManager.deleteSessionData(targetSessionToken) + + // success message + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.OK, + "text/plain", + "Session deleted" + ) + } + + // ignore other methods + else -> { + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, + "text/plain", + "Invalid method" + ) + } + } + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSession.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSession.kt index 84c4c37..99998cb 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSession.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSession.kt @@ -1,13 +1,13 @@ package com.faraphel.tasks_valider.connectivity.task.session +import com.faraphel.tasks_valider.database.entities.PersonEntity import kotlinx.serialization.Serializable /** * store the data of a session in the task system - * @param role the role accorded to the session */ @Serializable data class TaskSession( - var role: TaskRole = TaskRole.STUDENT -) + val person: PersonEntity, +) \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSessionManager.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSessionManager.kt index e0552c6..772bb6d 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSessionManager.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSessionManager.kt @@ -1,7 +1,6 @@ package com.faraphel.tasks_valider.connectivity.task.session -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken +import com.faraphel.tasks_valider.database.entities.PersonEntity import fi.iki.elonen.NanoHTTPD import java.util.* @@ -9,21 +8,37 @@ import java.util.* /** * The manager for the session system */ -class TaskSessionManager { +class TaskSessionManager(///< the admin person entity + private val adminPersonEntity: PersonEntity +) { private val sessions = mutableMapOf() ///< sessions specific data + private val adminPersonData = this.newSessionData(this.adminPersonEntity) ///< the session of the admin + val adminSessionToken = this.adminPersonData.first ///< the session token of the admin + val adminSession = this.adminPersonData.second ///< the session of the admin + + /** + * Get the admin person entity + * @return the admin person entity + */ + fun getAdminPersonEntity(): PersonEntity { + return this.adminPersonEntity + } + /** * Create a new session - * @param session the data for the session (optional) - * @param sessionId the session id to use (optional) - * @return a new session identifier + * @param person the person for the session (optional) + * @return a new session identifier and the corresponding session */ - fun newSessionData( - session: TaskSession = TaskSession(), - sessionId: String = UUID.randomUUID().toString() - ): String { - this.sessions[sessionId] = session - return sessionId + fun newSessionData(person: PersonEntity): Pair { + // create a new session token that is a secret random string + val sessionToken: String = UUID.randomUUID().toString() + // create a new session + val session = TaskSession(person) + // store the session + this.sessions[sessionToken] = session + // return the session data + return Pair(sessionToken, session) } /** @@ -31,54 +46,60 @@ class TaskSessionManager { * @param httpSession the HTTP session * @return the session data */ - fun getSessionData(httpSession: NanoHTTPD.IHTTPSession): TaskSession? { - val sessionId = httpSession.cookies.read("sessionId") ?: return null - val sessionData = this.getSessionData(sessionId) + fun getSessionData(httpSession: NanoHTTPD.IHTTPSession): Pair? { + // get the session token from the cookies + val sessionToken = httpSession.cookies.read("sessionToken") ?: return null + // get the session data + val sessionData = this.getSessionData(sessionToken) + // return the session data return sessionData } /** * Get data from a session identifier - * @param sessionId the identifier of the session + * @param sessionToken the identifier of the session * @return the session data */ - fun getSessionData(sessionId: String): TaskSession? { - return this.sessions[sessionId] + fun getSessionData(sessionToken: String): Pair? { + // get the session data + val session = this.sessions[sessionToken] + ?: return null + // return the session data + return Pair(sessionToken, session) } /** - * Set the data of a session - * @param sessionId the identifier of the session - * @param session the session data + * Update the session data + * @param sessionToken the identifier of the session + * @param session the new session data */ - fun setSessionData(sessionId: String, session: TaskSession) { - this.sessions[sessionId] = session + fun updateSessionData(sessionToken: String, session: TaskSession) { + // update the session + this.sessions[sessionToken] = session } /** * Delete a session - * @param sessionId the identifier of the session + * @param sessionToken the identifier of the session */ - fun deleteSessionData(sessionId: String): TaskSession? { - return this.sessions.remove(sessionId) + fun deleteSessionData(sessionToken: String): Pair? { + // remove the session + val session = this.sessions.remove(sessionToken) ?: + return null + // return the session data + return Pair(sessionToken, session) } /** * Get data from a http session. If it does not exist, create it. * @param httpSession the HTTP session */ - fun getOrCreateSessionData(httpSession: NanoHTTPD.IHTTPSession): TaskSession { + fun getOrCreateSessionData(httpSession: NanoHTTPD.IHTTPSession, person: PersonEntity): Pair { // try to get the session directly - var session = this.getSessionData(httpSession) - - // if the session does not exist, create it - if (session == null) { - val sessionId = this.newSessionData() - session = this.getSessionData(sessionId)!! - } - - // return the session - return session + val sessionData = this.getSessionData(httpSession) + ?: this.newSessionData(person) + // return the session data + return sessionData } /** @@ -88,10 +109,11 @@ class TaskSessionManager { */ fun responseSetSessionData( response: NanoHTTPD.Response, - cookies: NanoHTTPD.CookieHandler + cookies: NanoHTTPD.CookieHandler, + sessionToken: String ): NanoHTTPD.Response { // update the cookie of the user - cookies.set(NanoHTTPD.Cookie("sessionId", this.newSessionData())) + cookies.set(NanoHTTPD.Cookie("sessionToken", sessionToken)) // load them in the response cookies.unloadQueue(response) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/TaskDatabase.kt b/app/src/main/java/com/faraphel/tasks_valider/database/TaskDatabase.kt index f2ff03e..90be0bf 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/TaskDatabase.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/TaskDatabase.kt @@ -3,7 +3,7 @@ package com.faraphel.tasks_valider.database import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters -import com.faraphel.tasks_valider.database.converters.InstantConverter +import com.faraphel.tasks_valider.utils.converters.InstantConverter import com.faraphel.tasks_valider.database.dao.* import com.faraphel.tasks_valider.database.entities.* @@ -22,6 +22,7 @@ import com.faraphel.tasks_valider.database.entities.* ValidationEntity::class, RelationClassPersonEntity::class, + RelationPersonSessionSubjectEntity::class, ], version = 1 ) @@ -37,4 +38,5 @@ abstract class TaskDatabase: RoomDatabase() { abstract fun validationDao(): ValidationDao abstract fun relationClassPersonDao(): RelationClassPersonDao + abstract fun relationPersonSessionSubjectDao(): RelationPersonSessionSubjectDao } diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/TaskEntityHttpClient.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/TaskEntityHttpClient.kt new file mode 100644 index 0000000..4a40611 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/TaskEntityHttpClient.kt @@ -0,0 +1,98 @@ +package com.faraphel.tasks_valider.database.api.client + +import okhttp3.HttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody +import kotlin.time.Duration.Companion.seconds + + +/** + * An HTTP client to handle the room connection. + * @param address the address of the server + * @param port the port of the server + * @param baseCookies list of cookies to use (optional) + */ +class TaskEntityHttpClient( + private val address: String, + private val port: Int, + private val baseCookies: List = listOf() +) { + // the base url for the server + private val baseUrl = "http://$address:$port" + // the HTTP client + private val client = OkHttpClient().newBuilder() + .cookieJar( + object : okhttp3.CookieJar { + private val cookies = baseCookies.toMutableList() ///< list of cookies + override fun loadForRequest(url: HttpUrl) = this.cookies + override fun saveFromResponse(url: HttpUrl, cookies: List) { this.cookies.addAll(cookies) } + } + ) + .callTimeout(30.seconds) + .build() + + /** + * Return a basic request to the server + * @param endpoint the endpoint of the server + */ + private fun baseRequestBuilder(endpoint: String): okhttp3.Request.Builder = + okhttp3.Request.Builder().url("$baseUrl/$endpoint") + + /** + * Run a HEAD request + * @param endpoint the endpoint of the server + */ + fun head(endpoint: String): okhttp3.Request = + this.baseRequestBuilder(endpoint).head().build() + + /** + * Run a GET request + * @param endpoint the endpoint of the server + */ + fun get(endpoint: String): okhttp3.Response = + this.client.newCall( + this.baseRequestBuilder(endpoint) + .get() + .build() + ).execute() + + /** + * Run a POST request + * @param endpoint the endpoint of the server + * @param content the content of the request + * @param type the type of the content + */ + fun post(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response = + this.client.newCall( + this.baseRequestBuilder(endpoint) + .post(content.toRequestBody(type.toMediaType())) + .build() + ).execute() + + /** + * Run a PATCH request + * @param endpoint the endpoint of the server + * @param content the content of the request + * @param type the type of the content + */ + fun patch(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response = + this.client.newCall( + this.baseRequestBuilder(endpoint) + .patch(content.toRequestBody(type.toMediaType())) + .build() + ).execute() + + /** + * Run a DELETE request + * @param endpoint the endpoint of the server + * @param content the content of the request + * @param type the type of the content + */ + fun delete(endpoint: String, content: String, type: String = "text/plain"): okhttp3.Response = + this.client.newCall( + this.baseRequestBuilder(endpoint) + .delete(content.toRequestBody(type.toMediaType())) + .build() + ).execute() +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/ClassClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/ClassClientApi.kt new file mode 100644 index 0000000..dbfd6c0 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/ClassClientApi.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.database.api.client.entities + +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi +import com.faraphel.tasks_valider.database.entities.ClassEntity + + +class ClassClientApi( + client: TaskEntityHttpClient, +) : BaseClientApi( + client, + ClassEntity::class +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/PersonClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/PersonClientApi.kt new file mode 100644 index 0000000..c4fceb3 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/PersonClientApi.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.database.api.client.entities + +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi +import com.faraphel.tasks_valider.database.entities.PersonEntity + + +class PersonClientApi( + client: TaskEntityHttpClient, +) : BaseClientApi( + client, + PersonEntity::class +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/RelationClassPersonClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/RelationClassPersonClientApi.kt new file mode 100644 index 0000000..2736a61 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/RelationClassPersonClientApi.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.database.api.client.entities + +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi +import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity + + +class RelationClassPersonClientApi( + client: TaskEntityHttpClient, +) : BaseClientApi( + client, + RelationClassPersonEntity::class +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/RelationPersonSessionSubjectClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/RelationPersonSessionSubjectClientApi.kt new file mode 100644 index 0000000..c6dd7b0 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/RelationPersonSessionSubjectClientApi.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.database.api.client.entities + +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi +import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity + + +class RelationPersonSessionSubjectClientApi( + client: TaskEntityHttpClient, +) : BaseClientApi( + client, + RelationPersonSessionSubjectEntity::class +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/SessionClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/SessionClientApi.kt new file mode 100644 index 0000000..1cd904a --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/SessionClientApi.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.database.api.client.entities + +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi +import com.faraphel.tasks_valider.database.entities.SessionEntity + + +class SessionClientApi( + client: TaskEntityHttpClient, +) : BaseClientApi( + client, + SessionEntity::class +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/SubjectClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/SubjectClientApi.kt new file mode 100644 index 0000000..3fff5e4 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/SubjectClientApi.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.database.api.client.entities + +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi +import com.faraphel.tasks_valider.database.entities.SubjectEntity + + +class SubjectClientApi( + client: TaskEntityHttpClient, +) : BaseClientApi( + client, + SubjectEntity::class +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/TaskClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/TaskClientApi.kt new file mode 100644 index 0000000..66aeef1 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/TaskClientApi.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.database.api.client.entities + +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi +import com.faraphel.tasks_valider.database.entities.TaskEntity + + +class TaskClientApi( + client: TaskEntityHttpClient, +) : BaseClientApi( + client, + TaskEntity::class +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/ValidationClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/ValidationClientApi.kt new file mode 100644 index 0000000..14cb238 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/ValidationClientApi.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.database.api.client.entities + +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.api.client.entities.base.BaseClientApi +import com.faraphel.tasks_valider.database.entities.ValidationEntity + + +class ValidationClientApi( + client: TaskEntityHttpClient, +) : BaseClientApi( + client, + ValidationEntity::class +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/base/BaseClientApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/base/BaseClientApi.kt new file mode 100644 index 0000000..8f26072 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/client/entities/base/BaseClientApi.kt @@ -0,0 +1,100 @@ +package com.faraphel.tasks_valider.database.api.client.entities.base; + +import android.util.Log +import com.faraphel.tasks_valider.database.api.client.TaskEntityHttpClient +import com.faraphel.tasks_valider.database.entities.base.BaseEntity +import com.faraphel.tasks_valider.database.entities.error.HttpException +import com.faraphel.tasks_valider.utils.parser +import com.google.gson.reflect.TypeToken +import kotlin.reflect.KClass +import kotlin.reflect.full.companionObject +import kotlin.reflect.full.companionObjectInstance +import kotlin.reflect.full.declaredMemberProperties + + +abstract class BaseClientApi( + private val client: TaskEntityHttpClient, + private val entityType: KClass, +) { + /** + * return the API endpoint for this entity + * @return the API endpoint for this entity + */ + private fun getEndpoint(): String { + // get the property for the name of the table + val propertyTableName = entityType.companionObject!!.declaredMemberProperties.first { member -> + member.name == "TABLE_NAME" + } + // get the table name by calling the getter of the property + val tableName = propertyTableName.getter.call(entityType.companionObjectInstance) + // return the endpoint + return "entities/${tableName}" + } + + /** + * return all the entities for that table + * @return all the entities for that table + * @throws java.io.IOException reading error while parsing request + * @throws HttpException error of the request + */ + fun getAll(): List { + // try to obtain the list of validations + val response = client.get(this.getEndpoint()) + + // in case of error, notify it + if (!response.isSuccessful) + throw HttpException(response.code) + + val data = response.body.string() + + // parse the list of validations + return parser.fromJson( + data, + TypeToken.getParameterized(ArrayList::class.java, entityType.java).type + ) + } + + /** + * create a new entity in the table + * @return the id of the object in the database + * @throws java.io.IOException reading error while parsing request + * @throws HttpException error of the request + */ + fun save(entity: Entity): Long { + // try to send the serialized entity as json + val response = client.post( + this.getEndpoint(), + parser.toJson(entity), + "application/json; charset=utf-8" + ) + + // in case of error, notify it + if (!response.isSuccessful) + throw HttpException(response.code) + + // return the id of the object + return response.body.string().toLong() + } + + /** + * delete an entity in the table + * @return the number of object deleted in the database + * @throws java.io.IOException reading error while parsing request + * @throws HttpException error of the request + */ + fun delete(entity: Entity): Long { + // try to delete the object + val response = client.delete( + this.getEndpoint(), + parser.toJson(entity), + "application/json; charset=utf-8" + ) + + // in case of error, notify it + if (!response.isSuccessful) + throw HttpException(response.code) + + // return the id of the object + return response.body.string().toLong() + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/ClassApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/ClassApi.kt deleted file mode 100644 index a012aa2..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/ClassApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.faraphel.tasks_valider.database.api.entities - -import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.ClassEntity - -class ClassApi(dao: BaseDao) : BaseJsonApi(dao) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/PersonApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/PersonApi.kt deleted file mode 100644 index d9edf18..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/PersonApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.faraphel.tasks_valider.database.api.entities - -import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.PersonEntity - -class PersonApi(dao: BaseDao) : BaseJsonApi(dao) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/RelationClassPersonApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/RelationClassPersonApi.kt deleted file mode 100644 index 3f437b5..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/RelationClassPersonApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.faraphel.tasks_valider.database.api.entities - -import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity - -class RelationClassPersonApi(dao: BaseDao) : BaseJsonApi(dao) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/SessionApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/SessionApi.kt deleted file mode 100644 index 2eb7dc2..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/SessionApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.faraphel.tasks_valider.database.api.entities - -import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.SessionEntity - -class SessionApi(dao: BaseDao) : BaseJsonApi(dao) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/SubjectApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/SubjectApi.kt deleted file mode 100644 index caca63a..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/SubjectApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.faraphel.tasks_valider.database.api.entities - -import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.SubjectEntity - -class SubjectApi(dao: BaseDao) : BaseJsonApi(dao) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/TaskApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/TaskApi.kt deleted file mode 100644 index c331a98..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/TaskApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.faraphel.tasks_valider.database.api.entities - -import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.TaskEntity - -class TaskApi(dao: BaseDao) : BaseJsonApi(dao) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/ValidationApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/ValidationApi.kt deleted file mode 100644 index 60e3010..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/ValidationApi.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.faraphel.tasks_valider.database.api.entities - -import com.faraphel.tasks_valider.database.api.entities.base.BaseJsonApi -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.ValidationEntity - -class ValidationApi(dao: BaseDao) : BaseJsonApi(dao) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseJsonApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseJsonApi.kt deleted file mode 100644 index 4668feb..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseJsonApi.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.faraphel.tasks_valider.database.api.entities.base - -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.base.BaseEntity -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import fi.iki.elonen.NanoHTTPD - -/** - * A base for the API to handle the database operations. - * This is preconfigured to handle JSON data. - * @param Entity the entity type to handle - */ -abstract class BaseJsonApi(private val dao: BaseDao) : BaseApi { - companion object { - private val parser = Gson() ///< The JSON parser - } - - private val entityTypeToken: TypeToken = object: TypeToken() {} ///< the type of the managed entity - - // Requests - - override fun head(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response { - val obj = parser.fromJson( - session.inputStream.bufferedReader().readText(), - this.entityTypeToken.type - ) - val exists = this.dao.exists(obj) - - return NanoHTTPD.newFixedLengthResponse( - if (exists) NanoHTTPD.Response.Status.OK else NanoHTTPD.Response.Status.NOT_FOUND, - "text/plain", - if (exists) "Exists" else "Not found" - ) - } - - override fun get(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response { - return NanoHTTPD.newFixedLengthResponse( - NanoHTTPD.Response.Status.OK, - "application/json", - parser.toJson(this.dao.getAll()) - ) - } - - override fun post(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response { - val obj = parser.fromJson( - session.inputStream.bufferedReader().readText(), - this.entityTypeToken.type - ) - val id = this.dao.insert(obj) - - return NanoHTTPD.newFixedLengthResponse( - NanoHTTPD.Response.Status.CREATED, - "text/plain", - id.toString() - ) - } - - override fun delete(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response { - val obj = parser.fromJson( - session.inputStream.bufferedReader().readText(), - this.entityTypeToken.type - ) - val count = this.dao.delete(obj) - - return NanoHTTPD.newFixedLengthResponse( - if (count > 0) NanoHTTPD.Response.Status.OK else NanoHTTPD.Response.Status.NOT_FOUND, - "text/plain", - count.toString() - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/TaskDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/DatabaseApi.kt similarity index 65% rename from app/src/main/java/com/faraphel/tasks_valider/database/api/TaskDatabaseApi.kt rename to app/src/main/java/com/faraphel/tasks_valider/database/api/server/DatabaseApi.kt index 2303a34..61a54be 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/TaskDatabaseApi.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/DatabaseApi.kt @@ -1,23 +1,28 @@ -package com.faraphel.tasks_valider.database.api +package com.faraphel.tasks_valider.database.api.server import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission import com.faraphel.tasks_valider.connectivity.task.session.TaskSession import com.faraphel.tasks_valider.database.TaskDatabase -import com.faraphel.tasks_valider.database.api.entities.* -import com.faraphel.tasks_valider.database.api.entities.base.BaseApi +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseDatabaseApi +import com.faraphel.tasks_valider.database.api.server.entities.* import com.faraphel.tasks_valider.database.entities.* import fi.iki.elonen.NanoHTTPD -class TaskDatabaseApi(private val database: TaskDatabase) { - private val api: Map = mapOf( - ClassEntity.TABLE_NAME to ClassApi(this.database.classDao()), - PersonEntity.TABLE_NAME to PersonApi(this.database.personDao()), - SessionEntity.TABLE_NAME to SessionApi(this.database.sessionDao()), - SubjectEntity.TABLE_NAME to SubjectApi(this.database.subjectDao()), - TaskEntity.TABLE_NAME to TaskApi(this.database.taskDao()), - ValidationEntity.TABLE_NAME to ValidationApi(this.database.validationDao()), - RelationClassPersonEntity.TABLE_NAME to RelationClassPersonApi(this.database.relationClassPersonDao()), +class DatabaseApi( + private val database: TaskDatabase, + private val session: SessionEntity, +) { + private val api: Map = mapOf( + ClassEntity.TABLE_NAME to ClassDatabaseApi(this.database.classDao(), session), + PersonEntity.TABLE_NAME to PersonDatabaseApi(this.database.personDao(), session), + SessionEntity.TABLE_NAME to SessionDatabaseApi(this.database.sessionDao(), session), + SubjectEntity.TABLE_NAME to SubjectDatabaseApi(this.database.subjectDao(), session), + TaskEntity.TABLE_NAME to TaskDatabaseApi(database.taskDao(), session), + ValidationEntity.TABLE_NAME to ValidationDatabaseApi(this.database.validationDao(), session), + + RelationClassPersonEntity.TABLE_NAME to RelationClassPersonDatabaseApi(this.database.relationClassPersonDao(), session), + RelationPersonSessionSubjectEntity.TABLE_NAME to RelationPersonSessionSubjectDatabaseApi(this.database.relationPersonSessionSubjectDao(), session), ) /** @@ -27,10 +32,18 @@ class TaskDatabaseApi(private val database: TaskDatabase) { * @param path the path of the request */ fun handleRequest( - taskSession: TaskSession, + taskSession: TaskSession?, httpSession: NanoHTTPD.IHTTPSession, path: MutableList ): NanoHTTPD.Response { + // check if the user is authenticated + if (taskSession == null) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.UNAUTHORIZED, + "text/plain", + "Unauthorized" + ) + // get the entity name val entityName = path.removeFirstOrNull() ?: return NanoHTTPD.newFixedLengthResponse( @@ -53,7 +66,7 @@ class TaskDatabaseApi(private val database: TaskDatabase) { // TODO(Faraphel): should only be allowed to read data concerning the current class session NanoHTTPD.Method.HEAD -> { // check the permission of the session - if (taskSession.role.permissions.contains(TaskPermission.READ)) + if (!taskSession.person.role.permissions.contains(TaskPermission.READ)) return NanoHTTPD.newFixedLengthResponse( NanoHTTPD.Response.Status.FORBIDDEN, "text/plain", @@ -65,7 +78,7 @@ class TaskDatabaseApi(private val database: TaskDatabase) { // get the data from the database NanoHTTPD.Method.GET -> { // check the permission of the session - if (taskSession.role.permissions.contains(TaskPermission.READ)) + if (!taskSession.person.role.permissions.contains(TaskPermission.READ)) return NanoHTTPD.newFixedLengthResponse( NanoHTTPD.Response.Status.FORBIDDEN, "text/plain", @@ -77,7 +90,7 @@ class TaskDatabaseApi(private val database: TaskDatabase) { // insert the data into the database NanoHTTPD.Method.POST -> { // check the permission of the session - if (taskSession.role.permissions.contains(TaskPermission.WRITE)) + if (!taskSession.person.role.permissions.contains(TaskPermission.WRITE)) return NanoHTTPD.newFixedLengthResponse( NanoHTTPD.Response.Status.FORBIDDEN, "text/plain", @@ -89,7 +102,7 @@ class TaskDatabaseApi(private val database: TaskDatabase) { // delete the data from the database NanoHTTPD.Method.DELETE -> { // check the permission of the session - if (taskSession.role.permissions.contains(TaskPermission.WRITE)) + if (!taskSession.person.role.permissions.contains(TaskPermission.WRITE)) return NanoHTTPD.newFixedLengthResponse( NanoHTTPD.Response.Status.FORBIDDEN, "text/plain", diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/ClassDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/ClassDatabaseApi.kt new file mode 100644 index 0000000..af0b5e9 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/ClassDatabaseApi.kt @@ -0,0 +1,16 @@ +package com.faraphel.tasks_valider.database.api.server.entities + +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.ClassEntity +import com.faraphel.tasks_valider.database.entities.SessionEntity + + +class ClassDatabaseApi( + dao: BaseTaskDao, + session: SessionEntity +) : BaseTaskDatabaseApi( + dao, + session, + ClassEntity::class.java +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/PersonDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/PersonDatabaseApi.kt new file mode 100644 index 0000000..86ab57e --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/PersonDatabaseApi.kt @@ -0,0 +1,16 @@ +package com.faraphel.tasks_valider.database.api.server.entities + +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.PersonEntity +import com.faraphel.tasks_valider.database.entities.SessionEntity + + +class PersonDatabaseApi( + dao: BaseTaskDao, + session: SessionEntity +) : BaseTaskDatabaseApi( + dao, + session, + PersonEntity::class.java +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/RelationClassPersonDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/RelationClassPersonDatabaseApi.kt new file mode 100644 index 0000000..364429b --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/RelationClassPersonDatabaseApi.kt @@ -0,0 +1,16 @@ +package com.faraphel.tasks_valider.database.api.server.entities + +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity +import com.faraphel.tasks_valider.database.entities.SessionEntity + + +class RelationClassPersonDatabaseApi( + dao: BaseTaskDao, + session: SessionEntity +) : BaseTaskDatabaseApi( + dao, + session, + RelationClassPersonEntity::class.java +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/RelationPersonSessionSubjectDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/RelationPersonSessionSubjectDatabaseApi.kt new file mode 100644 index 0000000..76096bf --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/RelationPersonSessionSubjectDatabaseApi.kt @@ -0,0 +1,16 @@ +package com.faraphel.tasks_valider.database.api.server.entities + +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity +import com.faraphel.tasks_valider.database.entities.SessionEntity + + +class RelationPersonSessionSubjectDatabaseApi( + dao: BaseTaskDao, + session: SessionEntity +) : BaseTaskDatabaseApi( + dao, + session, + RelationPersonSessionSubjectEntity::class.java +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/SessionDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/SessionDatabaseApi.kt new file mode 100644 index 0000000..4ee1ca4 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/SessionDatabaseApi.kt @@ -0,0 +1,15 @@ +package com.faraphel.tasks_valider.database.api.server.entities + +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.SessionEntity + + +class SessionDatabaseApi( + dao: BaseTaskDao, + session: SessionEntity +) : BaseTaskDatabaseApi( + dao, + session, + SessionEntity::class.java +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/SubjectDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/SubjectDatabaseApi.kt new file mode 100644 index 0000000..badbe23 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/SubjectDatabaseApi.kt @@ -0,0 +1,16 @@ +package com.faraphel.tasks_valider.database.api.server.entities + +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.SessionEntity +import com.faraphel.tasks_valider.database.entities.SubjectEntity + + +class SubjectDatabaseApi( + dao: BaseTaskDao, + session: SessionEntity +) : BaseTaskDatabaseApi( + dao, + session, + SubjectEntity::class.java +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/TaskDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/TaskDatabaseApi.kt new file mode 100644 index 0000000..be98b86 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/TaskDatabaseApi.kt @@ -0,0 +1,16 @@ +package com.faraphel.tasks_valider.database.api.server.entities + +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.SessionEntity +import com.faraphel.tasks_valider.database.entities.TaskEntity + + +class TaskDatabaseApi( + dao: BaseTaskDao, + session: SessionEntity +) : BaseTaskDatabaseApi( + dao, + session, + TaskEntity::class.java +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/ValidationDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/ValidationDatabaseApi.kt new file mode 100644 index 0000000..6003df7 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/ValidationDatabaseApi.kt @@ -0,0 +1,16 @@ +package com.faraphel.tasks_valider.database.api.server.entities + +import com.faraphel.tasks_valider.database.api.server.entities.base.BaseTaskDatabaseApi +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.SessionEntity +import com.faraphel.tasks_valider.database.entities.ValidationEntity + + +class ValidationDatabaseApi( + dao: BaseTaskDao, + session: SessionEntity +) : BaseTaskDatabaseApi( + dao, + session, + ValidationEntity::class.java +) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/base/BaseDatabaseApi.kt similarity index 51% rename from app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseApi.kt rename to app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/base/BaseDatabaseApi.kt index d40665e..ae65321 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseApi.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/base/BaseDatabaseApi.kt @@ -1,32 +1,32 @@ -package com.faraphel.tasks_valider.database.api.entities.base +package com.faraphel.tasks_valider.database.api.server.entities.base import fi.iki.elonen.NanoHTTPD /** * A base for the API to handle the database operations with an HTTP server. */ -interface BaseApi { +interface BaseDatabaseApi { /** * Handle the HEAD request * This is used to check if a data exists in the database */ - fun head(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response + fun head(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response /** * Handle the GET request - * This is used to get data from the database + * This is used to get all the data of a table from the database */ - fun get(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response + fun get(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response /** * Handle the POST request * This is used to insert data into the database */ - fun post(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response + fun post(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response /** * Handle the PUT request * This is used to delete data from the database */ - fun delete(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response + fun delete(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response } \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/base/BaseTaskDatabaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/base/BaseTaskDatabaseApi.kt new file mode 100644 index 0000000..1acf9dd --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/server/entities/base/BaseTaskDatabaseApi.kt @@ -0,0 +1,95 @@ +package com.faraphel.tasks_valider.database.api.server.entities.base + +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.SessionEntity +import com.faraphel.tasks_valider.utils.getBody +import com.faraphel.tasks_valider.utils.parser +import fi.iki.elonen.NanoHTTPD + + +abstract class BaseTaskDatabaseApi ( + private val dao: BaseTaskDao, + private val session: SessionEntity, + private val entityType: Class, +) : BaseDatabaseApi { + /** + * Handle an HTTP HEAD request. + * Indicate if an object exist in the database. + * @param httpSession the current http session to handle. + * @return a response indicating if the object does exist. + */ + override fun head(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response { + // get the content of the request + val data = httpSession.getBody() + // parse the object + val obj = parser.fromJson(data, entityType) + // check if the object is in the object accessible from the session + val exists = this.dao.getAllBySession(session.id).contains(obj) + + // return the response + return NanoHTTPD.newFixedLengthResponse( + if (exists) NanoHTTPD.Response.Status.OK else NanoHTTPD.Response.Status.NOT_FOUND, + "text/plain", + if (exists) "Exists" else "Not found" + ) + } + + /** + * Handle an HTTP GET request. + * Indicate the content of a table in the database. + * @param httpSession the current http session to handle. + * @return a response indicating the content of all the objects. + */ + override fun get(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response { + // return the content of all the objects in the database about this session + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.OK, + "application/json", + parser.toJson(this.dao.getAllBySession(session.id)) + ) + } + + /** + * Handle an HTTP POST request. + * Create a new object in the database. + * @param httpSession the current http session to handle. + * @return the id of the newly created object + */ + override fun post(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response { + // get the content of the request + val data = httpSession.getBody() + // parse the object + val obj = parser.fromJson(data, entityType) + // insert it into the database + val id = this.dao.insert(obj) + + // return the id of the object created + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.CREATED, + "text/plain", + id.toString() + ) + } + + /** + * Handle an HTTP DELETE request. + * Delete an item from the database. + * @param httpSession the current http session to handle. + * @return the number of object deleted. + */ + override fun delete(httpSession: NanoHTTPD.IHTTPSession): NanoHTTPD.Response { + // get the content of the request + val data = httpSession.getBody() + // parse the object + val obj = parser.fromJson(data, entityType) + // delete all the instance of this object in the database + val count = this.dao.delete(obj) + + // return the number of corresponding element deleted + return NanoHTTPD.newFixedLengthResponse( + if (count > 0) NanoHTTPD.Response.Status.OK else NanoHTTPD.Response.Status.NOT_FOUND, + "text/plain", + count.toString() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/converters/InstantConverter.kt b/app/src/main/java/com/faraphel/tasks_valider/database/converters/InstantConverter.kt deleted file mode 100644 index 3a06a2d..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/converters/InstantConverter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.faraphel.tasks_valider.database.converters - -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.room.TypeConverter -import java.time.Instant - -class InstantConverter { - @RequiresApi(Build.VERSION_CODES.O) - @TypeConverter - fun fromTimestamp(value: Long?): Instant? { - return value?.let { Instant.ofEpochMilli(it) } - } - - @RequiresApi(Build.VERSION_CODES.O) - @TypeConverter - fun dateToTimestamp(instant: Instant?): Long? { - return instant?.toEpochMilli() - } -} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/ClassDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/ClassDao.kt index baef405..e7f2dab 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/ClassDao.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/ClassDao.kt @@ -2,22 +2,32 @@ package com.faraphel.tasks_valider.database.dao import androidx.room.Dao import androidx.room.Query -import com.faraphel.tasks_valider.database.dao.base.BaseDao +import com.faraphel.tasks_valider.database.dao.base.BaseCronDao +import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.entities.ClassEntity import com.faraphel.tasks_valider.database.entities.PersonEntity import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity import com.faraphel.tasks_valider.database.entities.SessionEntity @Dao -interface ClassDao : BaseDao { +interface ClassDao : BaseTaskDao { @Query("SELECT * FROM ${ClassEntity.TABLE_NAME}") override fun getAll(): List + @Query( + "SELECT * FROM ${ClassEntity.TABLE_NAME} " + + "JOIN ${SessionEntity.TABLE_NAME} " + + "ON ${SessionEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " + + "WHERE ${SessionEntity.TABLE_NAME}.id = :sessionId" + ) + override fun getAllBySession(sessionId: Long): List + /** * Get the object from its identifier */ @Query("SELECT * FROM ${ClassEntity.TABLE_NAME} WHERE id = :id") - fun getById(id: Long): ClassEntity + fun getById(id: Long): ClassEntity? /** * Get all the sessions this class attended @@ -31,7 +41,7 @@ interface ClassDao : BaseDao { * @param id the id of the class */ @Query( - "SELECT * FROM ${PersonEntity.TABLE_NAME} " + + "SELECT ${PersonEntity.TABLE_NAME}.* FROM ${PersonEntity.TABLE_NAME} " + "JOIN ${RelationClassPersonEntity.TABLE_NAME} " + "ON ${PersonEntity.TABLE_NAME}.id = ${RelationClassPersonEntity.TABLE_NAME}.student_id " + "WHERE ${RelationClassPersonEntity.TABLE_NAME}.class_id = :id" diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/PersonDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/PersonDao.kt index 601a2f6..7b481e8 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/PersonDao.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/PersonDao.kt @@ -2,11 +2,13 @@ package com.faraphel.tasks_valider.database.dao import androidx.room.Dao import androidx.room.Query -import com.faraphel.tasks_valider.database.dao.base.BaseDao +import com.faraphel.tasks_valider.database.dao.base.BaseCronDao +import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao import com.faraphel.tasks_valider.database.entities.* @Dao -interface PersonDao : BaseDao { +interface PersonDao : BaseTaskDao { @Query("SELECT * FROM ${PersonEntity.TABLE_NAME}") override fun getAll(): List @@ -14,7 +16,20 @@ interface PersonDao : BaseDao { * Get the object from its identifier */ @Query("SELECT * FROM ${PersonEntity.TABLE_NAME} WHERE id = :id") - fun getById(id: Long): PersonEntity + fun getById(id: Long): PersonEntity? + + @Query( + "SELECT * FROM ${PersonEntity.TABLE_NAME} " + + "WHERE id IN (" + + "SELECT student_id FROM ${RelationClassPersonEntity.TABLE_NAME} " + + "JOIN ${ClassEntity.TABLE_NAME} " + + "ON ${RelationClassPersonEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " + + "JOIN ${SessionEntity.TABLE_NAME} " + + "ON ${SessionEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " + + "WHERE ${SessionEntity.TABLE_NAME}.id = :sessionId" + + ")" + ) + override fun getAllBySession(sessionId: Long): List /** * Allow to get all the classes the person is attending as a student diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/RelationClassPersonDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/RelationClassPersonDao.kt index b0296df..3f77a53 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/RelationClassPersonDao.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/RelationClassPersonDao.kt @@ -2,22 +2,35 @@ package com.faraphel.tasks_valider.database.dao import androidx.room.Dao import androidx.room.Query -import com.faraphel.tasks_valider.database.dao.base.BaseDao +import com.faraphel.tasks_valider.database.dao.base.BaseCronDao +import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.ClassEntity import com.faraphel.tasks_valider.database.entities.RelationClassPersonEntity +import com.faraphel.tasks_valider.database.entities.SessionEntity @Dao -interface RelationClassPersonDao : BaseDao { +interface RelationClassPersonDao : BaseTaskDao { @Query("SELECT * FROM ${RelationClassPersonEntity.TABLE_NAME}") override fun getAll(): List + @Query( + "SELECT ${RelationClassPersonEntity.TABLE_NAME}.* FROM ${RelationClassPersonEntity.TABLE_NAME} " + + "JOIN ${ClassEntity.TABLE_NAME} " + + "ON ${RelationClassPersonEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " + + "JOIN ${SessionEntity.TABLE_NAME} " + + "ON ${SessionEntity.TABLE_NAME}.class_id = ${ClassEntity.TABLE_NAME}.id " + + "WHERE ${SessionEntity.TABLE_NAME}.id = :sessionId" + ) + override fun getAllBySession(sessionId: Long): List + /** * Get the object from its identifiers */ @Query( "SELECT * FROM ${RelationClassPersonEntity.TABLE_NAME} " + - "WHERE " + - "class_id = :classId AND " + - "student_id = :studentId" + "WHERE class_id = :classId " + + "AND student_id = :studentId" ) - fun getById(classId: Long, studentId: Long): RelationClassPersonEntity + fun getById(classId: Long, studentId: Long): RelationClassPersonEntity? } diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/RelationPersonSessionSubjectDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/RelationPersonSessionSubjectDao.kt new file mode 100644 index 0000000..45eede5 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/RelationPersonSessionSubjectDao.kt @@ -0,0 +1,32 @@ +package com.faraphel.tasks_valider.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.faraphel.tasks_valider.database.dao.base.BaseCronDao +import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity + + +@Dao +interface RelationPersonSessionSubjectDao : BaseTaskDao { + @Query("SELECT * FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME}") + override fun getAll(): List + + @Query( + "SELECT * FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " + + "WHERE session_id = :sessionId" + ) + override fun getAllBySession(sessionId: Long): List + + /** + * Get the object from its identifiers + */ + @Query( + "SELECT * FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " + + "WHERE student_id = :studentId " + + "AND session_id = :sessionId " + + "AND subject_id = :subjectId" + ) + fun getById(studentId: Long, sessionId: Long, subjectId: Long): RelationPersonSessionSubjectEntity? +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/SessionDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/SessionDao.kt index 8054915..29cc8df 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/SessionDao.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/SessionDao.kt @@ -2,17 +2,23 @@ package com.faraphel.tasks_valider.database.dao import androidx.room.Dao import androidx.room.Query -import com.faraphel.tasks_valider.database.dao.base.BaseDao +import com.faraphel.tasks_valider.database.dao.base.BaseCronDao +import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.ClassEntity import com.faraphel.tasks_valider.database.entities.SessionEntity @Dao -interface SessionDao : BaseDao { +interface SessionDao : BaseTaskDao { @Query("SELECT * FROM ${SessionEntity.TABLE_NAME}") override fun getAll(): List + @Query("SELECT * FROM ${SessionEntity.TABLE_NAME} WHERE id = :sessionId") + override fun getAllBySession(sessionId: Long): List + /** * Get the object from its identifier */ @Query("SELECT * FROM ${SessionEntity.TABLE_NAME} WHERE id = :id") - fun getById(id: Long): SessionEntity + fun getById(id: Long): SessionEntity? } diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/SubjectDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/SubjectDao.kt index 8dc676a..fbe8900 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/SubjectDao.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/SubjectDao.kt @@ -2,26 +2,38 @@ package com.faraphel.tasks_valider.database.dao import androidx.room.Dao import androidx.room.Query -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.SessionEntity +import com.faraphel.tasks_valider.database.dao.base.BaseCronDao +import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.ClassEntity +import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity import com.faraphel.tasks_valider.database.entities.SubjectEntity import com.faraphel.tasks_valider.database.entities.TaskEntity @Dao -interface SubjectDao : BaseDao { +interface SubjectDao : BaseTaskDao { @Query("SELECT * FROM ${SubjectEntity.TABLE_NAME}") override fun getAll(): List + @Query( + "SELECT * FROM ${SubjectEntity.TABLE_NAME} " + + "WHERE id IN (" + + "SELECT subject_id FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " + + "WHERE session_id = :sessionId" + + ")" + ) + override fun getAllBySession(sessionId: Long): List + /** * Get the object from its identifier */ @Query("SELECT * FROM ${SubjectEntity.TABLE_NAME} WHERE id = :id") - fun getById(id: Long): SubjectEntity + fun getById(id: Long): SubjectEntity? /** * Get all the tasks available in a subject * @param id the id of the subject */ @Query("SELECT * FROM ${TaskEntity.TABLE_NAME} WHERE subject_id = :id") - fun getSessions(id: Long): List + fun getTasks(id: Long): List } diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/TaskDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/TaskDao.kt index 0053cd0..6d68256 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/TaskDao.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/TaskDao.kt @@ -2,20 +2,32 @@ package com.faraphel.tasks_valider.database.dao import androidx.room.Dao import androidx.room.Query -import com.faraphel.tasks_valider.database.dao.base.BaseDao +import com.faraphel.tasks_valider.database.dao.base.BaseCronDao +import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity import com.faraphel.tasks_valider.database.entities.TaskEntity import com.faraphel.tasks_valider.database.entities.ValidationEntity @Dao -interface TaskDao : BaseDao { +interface TaskDao : BaseTaskDao { @Query("SELECT * FROM ${TaskEntity.TABLE_NAME}") override fun getAll(): List + @Query( + "SELECT * FROM ${TaskEntity.TABLE_NAME} " + + "WHERE subject_id IN (" + + "SELECT subject_id FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " + + "WHERE session_id = :sessionId" + + ")" + ) + override fun getAllBySession(sessionId: Long): List + /** * Get the object from its identifier */ @Query("SELECT * FROM ${TaskEntity.TABLE_NAME} WHERE id = :id") - fun getById(id: Long): TaskEntity + fun getById(id: Long): TaskEntity? /** * Get all the validations have been approved for this tasks diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/ValidationDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/ValidationDao.kt index 544da65..81262cc 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/ValidationDao.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/ValidationDao.kt @@ -2,14 +2,29 @@ package com.faraphel.tasks_valider.database.dao import androidx.room.Dao import androidx.room.Query -import com.faraphel.tasks_valider.database.dao.base.BaseDao +import com.faraphel.tasks_valider.database.dao.base.BaseCronDao +import com.faraphel.tasks_valider.database.dao.base.BaseSessionDao +import com.faraphel.tasks_valider.database.dao.base.BaseTaskDao +import com.faraphel.tasks_valider.database.entities.RelationPersonSessionSubjectEntity +import com.faraphel.tasks_valider.database.entities.TaskEntity import com.faraphel.tasks_valider.database.entities.ValidationEntity @Dao -interface ValidationDao : BaseDao { +interface ValidationDao : BaseTaskDao { @Query("SELECT * FROM ${ValidationEntity.TABLE_NAME}") override fun getAll(): List + @Query( + "SELECT * FROM ${ValidationEntity.TABLE_NAME} " + + "JOIN ${TaskEntity.TABLE_NAME} " + + "ON ${ValidationEntity.TABLE_NAME}.task_id = ${TaskEntity.TABLE_NAME}.id " + + "WHERE ${TaskEntity.TABLE_NAME}.subject_id IN (" + + "SELECT subject_id FROM ${RelationPersonSessionSubjectEntity.TABLE_NAME} " + + "WHERE session_id = :sessionId" + + ")" + ) + override fun getAllBySession(sessionId: Long): List + /** * Get the object from its identifiers */ @@ -20,5 +35,5 @@ interface ValidationDao : BaseDao { "student_id = :studentId and " + "task_id = :taskId" ) - fun getById(teacherId: Long, studentId: Long, taskId: Long): ValidationEntity + fun getById(teacherId: Long, studentId: Long, taskId: Long): ValidationEntity? } diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseCronDao.kt similarity index 61% rename from app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseDao.kt rename to app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseCronDao.kt index b06e62f..483bfad 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseDao.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseCronDao.kt @@ -3,17 +3,20 @@ package com.faraphel.tasks_valider.database.dao.base import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy -import androidx.room.Update -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken - /** - * A base DAO to handle the database operations. + * A base DAO to handle the database operations (CRON). * @param Entity the entity to handle */ -interface BaseDao { +interface BaseCronDao { + /** + * Check if the entity exists in the database. + */ + fun exists(entity: Entity): Boolean { + return this.getAll().contains(entity) + } + /** * Check if the entities exists in the database. */ @@ -21,12 +24,24 @@ interface BaseDao { return this.getAll().containsAll(entities.toList()) } + /** + * Insert the entities into the database. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(entity: Entity): Long + /** * Insert the entities into the database. */ @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg entities: Entity): List + /** + * Delete the entity from the database. + */ + @Delete + fun delete(entity: Entity): Int + /** * Delete the entities from the database. */ diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseSessionDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseSessionDao.kt new file mode 100644 index 0000000..1e457c4 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseSessionDao.kt @@ -0,0 +1,11 @@ +package com.faraphel.tasks_valider.database.dao.base + +/** + * The base for a DAO that is inside a session scope. + */ +interface BaseSessionDao { + /** + * Return all the objects concerned by a session. + */ + fun getAllBySession(sessionId: Long): List +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseTaskDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseTaskDao.kt new file mode 100644 index 0000000..c55a6cd --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/base/BaseTaskDao.kt @@ -0,0 +1,6 @@ +package com.faraphel.tasks_valider.database.dao.base + +/** + * The base for a DAO for the task project. + */ +interface BaseTaskDao: BaseCronDao, BaseSessionDao diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/PersonEntity.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/PersonEntity.kt index 2ee3506..0e62606 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/PersonEntity.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/PersonEntity.kt @@ -3,17 +3,59 @@ package com.faraphel.tasks_valider.database.entities import androidx.room.ColumnInfo 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 kotlinx.serialization.Serializable +import java.security.MessageDigest 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: UUID, + @ColumnInfo("card_id") val cardId: String? = null, + @ColumnInfo("password_hash") val passwordHash: String? = null, + @ColumnInfo("role") val role: TaskRole = TaskRole.STUDENT, ) : BaseEntity() { companion object { const val TABLE_NAME = "persons" + + /** + * Hash a password + */ + fun hashPassword(password: String): String { + val digester = MessageDigest.getInstance("SHA-256") + val hash = digester.digest(password.toByteArray()) + return hash.joinToString("") { byte -> "%02x".format(byte) } + } } + + /** + * Constructor with string password + */ + constructor( + firstName: String, + lastName: String, + cardId: String? = null, + password: String? = null, + role: TaskRole, + ) : this( + firstName = firstName, + lastName = lastName, + cardId = cardId, + passwordHash = password?.let { hashPassword(password) }, + role = role, + ) + + /** + * Return the full name of the person + */ + fun fullName(): String = "${this.firstName.capitalize(Locale.ROOT)} ${this.lastName.uppercase()}" + + /** + * Check if the password is correct + */ + fun checkPassword(password: String): Boolean = this.passwordHash == hashPassword(password) } diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/RelationPersonSessionSubjectEntity.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/RelationPersonSessionSubjectEntity.kt new file mode 100644 index 0000000..d39a8e9 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/RelationPersonSessionSubjectEntity.kt @@ -0,0 +1,48 @@ +package com.faraphel.tasks_valider.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import com.faraphel.tasks_valider.database.entities.base.BaseEntity + + +/** + * Represent the relation that associate a subject to a person for a specific session. + */ +@Entity( + tableName = RelationPersonSessionSubjectEntity.TABLE_NAME, + primaryKeys = [ + "student_id", + "session_id", + "subject_id" + ], + foreignKeys = [ + ForeignKey( + entity = PersonEntity::class, + parentColumns = ["id"], + childColumns = ["student_id"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = SessionEntity::class, + parentColumns = ["id"], + childColumns = ["session_id"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = SubjectEntity::class, + parentColumns = ["id"], + childColumns = ["subject_id"], + onDelete = ForeignKey.CASCADE + ), + ] +) +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, +) : BaseEntity() { + companion object { + const val TABLE_NAME = "relation_person_session_subject" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/TaskEntity.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/TaskEntity.kt index 4548c36..255fce3 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/TaskEntity.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/TaskEntity.kt @@ -21,6 +21,7 @@ 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("subject_id", index = true) val subjectId: Long, ) : BaseEntity() { diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/ValidationEntity.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/ValidationEntity.kt index 467f5df..7e489ca 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/ValidationEntity.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/ValidationEntity.kt @@ -35,14 +35,28 @@ import java.time.Instant ] ) data class ValidationEntity ( - @ColumnInfo("date") val date: Instant, - @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("date") val date: Instant, ) : BaseEntity() { companion object { const val TABLE_NAME = "validations" } -} + /** + * Construct a new ValidationEntity. Automatically set the date to today. + */ + constructor( + teacherId: Long, + studentId: Long, + taskId: Long + ): + this( + teacherId = teacherId, + studentId = studentId, + taskId = taskId, + date = Instant.now() + ) +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/base/BaseEntity.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/base/BaseEntity.kt index 44654b2..a00656f 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/base/BaseEntity.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/base/BaseEntity.kt @@ -1,3 +1,7 @@ package com.faraphel.tasks_valider.database.entities.base -open class BaseEntity +open class BaseEntity { + companion object { + const val TABLE_NAME = "" + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/error/HttpException.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/error/HttpException.kt new file mode 100644 index 0000000..0c28d91 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/error/HttpException.kt @@ -0,0 +1,6 @@ +package com.faraphel.tasks_valider.database.entities.error + + +class HttpException( + private val code: Int, +) : Exception("Http Exception: $code") \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/test.kt b/app/src/main/java/com/faraphel/tasks_valider/database/test.kt new file mode 100644 index 0000000..ff027fb --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/test.kt @@ -0,0 +1,133 @@ +package com.faraphel.tasks_valider.database + +import com.faraphel.tasks_valider.connectivity.task.session.TaskRole +import com.faraphel.tasks_valider.database.entities.* + +fun populateTaskDatabaseTest(database: TaskDatabase) { + // classes + val ( + classMiageId, + classIsriId, + ) = database.classDao().insert( + ClassEntity(name="MIAGE"), + ClassEntity(name="ISRI") + ) + + val classMiage = database.classDao().getById(classMiageId)!! + val classIrsi = database.classDao().getById(classIsriId)!! + + // persons + val ( + personBillyId, + personBobbyId, + personBettyId, + ) = database.personDao().insert( + PersonEntity( + "Billy", "Bob", + "0A1A7553-9DE5-103C-B23C-630998207116", + "1234", + TaskRole.STUDENT + ), + PersonEntity( + "Bobby", "Bob", + null, + "1234", + TaskRole.STUDENT + ), + PersonEntity( + "Betty", "Bob", + null, + "1234", + TaskRole.STUDENT + ) + ) + + val personBilly = database.personDao().getById(personBillyId)!! + val personBobby = database.personDao().getById(personBobbyId)!! + val personBetty = database.personDao().getById(personBettyId)!! + + // relations class <=> persons + database.relationClassPersonDao().insert( + RelationClassPersonEntity(personBobby.id, classMiage.id), + RelationClassPersonEntity(personBilly.id, classMiage.id), + RelationClassPersonEntity(personBetty.id, classIrsi.id), + ) + + // subjects + val ( + subjectAId, + subjectBId + ) = database.subjectDao().insert( + SubjectEntity( + name="Type A" + ), + SubjectEntity( + name="Type B" + ) + ) + + val subjectA = database.subjectDao().getById(subjectAId)!! + val subjectB = database.subjectDao().getById(subjectBId)!! + + // tasks + val ( + taskA1Id, + taskA2Id, + taskA3Id, + taskB1Id, + taskB2Id, + ) = database.taskDao().insert( + TaskEntity( + title = "Commencer A", + description = "Description 1", + order = 1, + subjectId = subjectA.id, + ), + TaskEntity( + title = "Continuer A", + description = "Description 2", + order = 2, + subjectId = subjectA.id + ), + TaskEntity( + title = "Finir A", + description = "Description 3", + order = 3, + subjectId = subjectA.id + ), + TaskEntity( + title = "Commencer B", + description = "Description 1", + order = 1, + subjectId = subjectB.id, + ), + TaskEntity( + title = "Finir B", + description = "Description 2", + order = 2, + subjectId = subjectB.id, + ) + ) + + val taskA1 = database.taskDao().getById(taskA1Id)!! + val taskA2 = database.taskDao().getById(taskA2Id)!! + val taskA3 = database.taskDao().getById(taskA3Id)!! + val taskB1 = database.taskDao().getById(taskB1Id)!! + val taskB2 = database.taskDao().getById(taskB2Id)!! +} + + +fun populateSubjectSessionPersonTest(database: TaskDatabase, session: SessionEntity) { + // get the list of available subjects + val subjects = database.subjectDao().getAll() + + // give a random subject to everyone in the session + database.personDao().getAllBySession(session.id).forEach { person -> + // get a random subject + val subject = subjects.random() + // insert a new subject for a person for a specific session + database.relationPersonSessionSubjectDao().insert( + RelationPersonSessionSubjectEntity(person.id, session.id, subject.id) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/authentification/client.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/authentification/client.kt new file mode 100644 index 0000000..1c83eb0 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/authentification/client.kt @@ -0,0 +1,40 @@ +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") + } + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/authentification/server.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/authentification/server.kt new file mode 100644 index 0000000..f9d5084 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/authentification/server.kt @@ -0,0 +1,70 @@ +package com.faraphel.tasks_valider.ui.screen.authentification + +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.unit.dp +import androidx.compose.ui.unit.sp +import com.faraphel.tasks_valider.connectivity.task.session.TaskRole +import com.faraphel.tasks_valider.database.entities.PersonEntity + + +/** + * Authentification screen where the host can give his information + */ +@Composable +fun AuthentificationServerScreen(personEntity: MutableState) { + val firstName = remember { mutableStateOf("") } + val lastName = remember { mutableStateOf("") } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // title + Text( + text = "Your Profile", + fontSize = 32.sp + ) + + // separator + Spacer(modifier = Modifier.height(24.dp)) + + // first name + TextField( + value = firstName.value, + placeholder = { Text("first name") }, + onValueChange = { text -> firstName.value = text }, + ) + + // last name + TextField( + value = lastName.value, + placeholder = { Text("last name") }, + onValueChange = { text -> lastName.value = text }, + ) + + // separator + Spacer(modifier = Modifier.height(24.dp)) + + // confirm button + Button(onClick = { + // create the person entity with the given information + personEntity.value = PersonEntity( + firstName = firstName.value, + lastName = lastName.value, + role = TaskRole.ADMIN + ) + }) { + Text("Submit") + } + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/client/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/client.kt similarity index 88% rename from app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/client/screen.kt rename to app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/client.kt index 2cc75b7..5a7927d 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/client/screen.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/client.kt @@ -1,6 +1,8 @@ -package com.faraphel.tasks_valider.ui.screen.communication.internet.client +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 @@ -16,15 +18,16 @@ import com.faraphel.tasks_valider.connectivity.task.TaskClient import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_ADDRESS import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT -import com.faraphel.tasks_valider.ui.screen.task.TaskSessionScreen +@RequiresApi(Build.VERSION_CODES.O) @Composable fun CommunicationInternetClientScreen(activity: Activity) { val client = remember { mutableStateOf(null) } - if (client.value == null) CommunicationInternetClientContent(client) - else TaskSessionScreen(activity, client.value!!) + // TODO(Faraphel): fix and get a user + // if (client.value == null) CommunicationInternetClientContent(client) + // else TaskSessionScreen(activity, client.value!!, user) } diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/selection.kt similarity index 57% rename from app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/screen.kt rename to app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/selection.kt index 335c2f7..1575f12 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/screen.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/selection.kt @@ -1,33 +1,52 @@ package com.faraphel.tasks_valider.ui.screen.communication.internet import android.app.Activity -import androidx.compose.foundation.layout.Column +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.* import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +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.ui.screen.communication.internet.client.CommunicationInternetClientScreen -import com.faraphel.tasks_valider.ui.screen.communication.internet.server.CommunicationInternetServerScreen +import com.faraphel.tasks_valider.database.TaskDatabase +@RequiresApi(Build.VERSION_CODES.O) @Composable -fun CommunicationInternetScreen(activity: Activity) { +fun CommunicationInternetSelectScreen(activity: Activity, database: TaskDatabase) { val controller = rememberNavController() NavHost(navController = controller, startDestination = "mode") { composable("mode") { CommunicationInternetSelectContent(controller) } composable("client") { CommunicationInternetClientScreen(activity) } - composable("server") { CommunicationInternetServerScreen(activity) } + composable("server") { CommunicationInternetServerScreen(activity, database) } } } @Composable fun CommunicationInternetSelectContent(controller: NavController) { - Column { + 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 diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/server.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/server.kt new file mode 100644 index 0000000..0d29936 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/server.kt @@ -0,0 +1,218 @@ +package com.faraphel.tasks_valider.ui.screen.communication.internet + +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 +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.* +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.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.authentification.AuthentificationServerScreen +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 +import java.time.Instant + + +/** + * Screen for the host to configure the server + */ +@RequiresApi(Build.VERSION_CODES.O) +@Composable +fun CommunicationInternetServerScreen( + activity: Activity, + database: TaskDatabase, +) { + val controller = rememberNavController() + + val adminPersonEntityRaw = remember { mutableStateOf(null) } + val adminPersonEntity = remember { mutableStateOf(null) } + val client = remember { mutableStateOf(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) AuthentificationServerScreen(adminPersonEntityRaw) + else controller.navigate("configuration") + } + composable("configuration") { + if (client.value == null) + CommunicationInternetServerContent( + database, + adminPersonEntityRaw, + adminPersonEntity, + client, + ) + else controller.navigate("session") + } + composable("session") { + TaskSessionController( + activity, + client.value!!, + adminPersonEntity.value!! + ) + } + } +} + + +@RequiresApi(Build.VERSION_CODES.O) +@Composable +fun CommunicationInternetServerContent( + database: TaskDatabase, + adminPersonEntityRaw: MutableState, + adminPersonEntity: MutableState, + client: MutableState +) { + val classes = remember { mutableStateOf?>(null) } + val selectedClass = remember { mutableStateOf(null) } + val areClassesExpanded = remember { mutableStateOf(false) } + + val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) } + + 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 = "") + } + + // 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 + } + ) + } + } + } + + // server port + Row(verticalAlignment = Alignment.CenterVertically) { + // descriptor + Text(text = "Port") + // separator + Spacer(modifier = Modifier.width(width = 12.dp)) + // input + TextField( + modifier = Modifier.width(80.dp), + value = serverPort.intValue.toString(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true, + onValueChange = { text -> + val port = text.toInt() + if (port in RANGE_SERVER_PORT) { + serverPort.intValue = port + } + } + ) + } + + // 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 + Thread { + populateSubjectSessionPersonTest(database, session) + }.let { thread -> + thread.start() + thread.join() + } + + // Create the server + Log.i("room-server", "creating the server") + val server = TaskServer( + serverPort.intValue, + 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?>) { + classes.value = database.classDao().getAll() +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/server/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/server/screen.kt deleted file mode 100644 index cdf8f13..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/server/screen.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.faraphel.tasks_valider.ui.screen.communication.internet.server - -import android.app.Activity -import android.util.Log -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -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 androidx.room.Room -import com.faraphel.tasks_valider.connectivity.task.TaskClient -import com.faraphel.tasks_valider.connectivity.task.TaskServer -import com.faraphel.tasks_valider.database.TaskDatabase -import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT -import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT -import com.faraphel.tasks_valider.ui.screen.task.TaskSessionScreen - - -@Composable -fun CommunicationInternetServerScreen(activity: Activity) { - val client = remember { mutableStateOf(null) } - - // if the server is not created, prompt the user for the server configuration - if (client.value == null) CommunicationInternetServerContent(activity, client) - // else, go to the base tasks screen - else TaskSessionScreen(activity, client.value!!) -} - - -@Composable -fun CommunicationInternetServerContent(activity: Activity, client: MutableState) { - 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 = { - // Reset the database | TODO(Faraphel): only for testing purpose - activity.deleteDatabase("local") - - // Create the database - val database = Room.databaseBuilder( - activity, - TaskDatabase::class.java, - "local" - ).build() - - // Create the server - Log.i("room-server", "creating the server") - Thread { // a thread is used for networking - val server = TaskServer(serverPort.intValue, database) - server.start() - - // Get the client from the server - client.value = server.getClientAdmin() - }.start() - }) { - Text("Create") - } - } -} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/selection.kt similarity index 63% rename from app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/screen.kt rename to app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/selection.kt index bb260ce..a04f0ae 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/screen.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/selection.kt @@ -1,19 +1,27 @@ package com.faraphel.tasks_valider.ui.screen.communication import android.app.Activity +import android.os.Build import android.widget.Toast -import androidx.compose.foundation.layout.Column +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.* import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +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.bwf.BwfManager -import com.faraphel.tasks_valider.ui.screen.communication.internet.CommunicationInternetScreen +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 @@ -21,10 +29,13 @@ import com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.CommunicationW * CommunicationController is the main controller for the communication screen. * It is responsible for handling the navigation between the different communication methods. * It is also responsible for initializing the communication methods. + * * @param activity: The activity that hosts the communication screen. + * @param database: the database. */ +@RequiresApi(Build.VERSION_CODES.O) @Composable -fun CommunicationScreen(activity: Activity) { +fun CommunicationModeSelectionScreen(activity: Activity, database: TaskDatabase) { val controller = rememberNavController() NavHost( @@ -35,11 +46,11 @@ fun CommunicationScreen(activity: Activity) { CommunicationSelectContent(controller, activity) } composable("internet") { - CommunicationInternetScreen(activity) + CommunicationInternetSelectScreen(activity, database) } composable("wifi-p2p") { - val bwfManager = BwfManager.fromActivity(activity) - CommunicationWifiP2pScreen(activity, bwfManager) + val bwdManager = BwdManager.fromActivity(activity) + CommunicationWifiP2pScreen(activity, bwdManager) } } } @@ -50,9 +61,22 @@ fun CommunicationScreen(activity: Activity) { */ @Composable fun CommunicationSelectContent(controller: NavController, activity: Activity) { - val isWifiP2pSupported = BwfManager.isSupported(activity) + val isWifiP2pSupported = BwdManager.isSupported(activity) - Column { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // title + Text( + text = "Connection Type", + fontSize = 32.sp + ) + + // separator + Spacer(modifier = Modifier.height(24.dp)) + // internet communication mode Button(onClick = { controller.navigate("internet") }) { Text("Internet") @@ -62,7 +86,7 @@ fun CommunicationSelectContent(controller: NavController, activity: Activity) { Button( colors = ButtonDefaults.buttonColors( // if the WiFi-Direct is not supported, the button is grayed out - containerColor = if (isWifiP2pSupported) Color.Unspecified else Color.Gray + containerColor = if (isWifiP2pSupported) MaterialTheme.colorScheme.primary else Color.Gray ), onClick = { // if the WiFi-Direct is supported, navigate to the WiFi-Direct screen diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/client/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/client/screen.kt index 3ba8cdd..b2cc732 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/client/screen.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/client/screen.kt @@ -8,12 +8,12 @@ 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.bwf.BwfManager +import com.faraphel.tasks_valider.connectivity.bwd.BwdManager import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget @Composable -fun CommunicationWifiP2pClientScreen(activity: Activity, bwfManager: BwfManager) { +fun CommunicationWifiP2pClientScreen(activity: Activity, bwdManager: BwdManager) { val selectedDevice = remember { mutableStateOf(null) } val isConnected = remember { mutableStateOf(false) } @@ -31,25 +31,25 @@ fun CommunicationWifiP2pClientScreen(activity: Activity, bwfManager: BwfManager) val config = WifiP2pConfig().apply { deviceAddress = selectedDevice.value!!.deviceAddress } - bwfManager.connect(config) { + bwdManager.connect(config) { isConnected.value = true } return } // display the list of devices - CommunicationWifiP2pClientContent(bwfManager, selectedDevice) + CommunicationWifiP2pClientContent(bwdManager, selectedDevice) } @Composable fun CommunicationWifiP2pClientContent( - bwfManager: BwfManager, + bwdManager: BwdManager, selectedDevice: MutableState ) { Column { WifiP2pDeviceListWidget( - peers = bwfManager.statePeers.value, + peers = bwdManager.statePeers.value, filter = { device: WifiP2pDevice -> device.isGroupOwner }, selectedDevice, ) diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/screen.kt index bd21fef..c7d4b58 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/screen.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/screen.kt @@ -9,19 +9,19 @@ 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.bwf.BwfManager +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, bwfManager: BwfManager) { +fun CommunicationWifiP2pScreen(activity: Activity, bwdManager: BwdManager) { val controller = rememberNavController() NavHost(navController = controller, startDestination = "mode") { composable("mode") { CommunicationWifiP2pSelectContent(controller) } - composable("client") { CommunicationWifiP2pClientScreen(activity, bwfManager) } - composable("server") { CommunicationWifiP2pServerScreen(activity, bwfManager) } + composable("client") { CommunicationWifiP2pClientScreen(activity, bwdManager) } + composable("server") { CommunicationWifiP2pServerScreen(activity, bwdManager) } } } diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/server/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/server/screen.kt index f608bc0..b3eba6d 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/server/screen.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/server/screen.kt @@ -1,47 +1,33 @@ package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.server import android.app.Activity -import android.util.Log -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -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 androidx.room.Room -import com.faraphel.tasks_valider.connectivity.bwf.BwfManager +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.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.TaskSessionScreen @Composable -fun CommunicationWifiP2pServerScreen(activity: Activity, bwfManager: BwfManager) { +fun CommunicationWifiP2pServerScreen(activity: Activity, bwdManager: BwdManager) { val client = remember { mutableStateOf(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) + // if (client.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, client) // else, go to the base tasks screen - else TaskSessionScreen(activity, client.value!!) + // else TaskSessionScreen(activity, client.value!!) } @Composable fun CommunicationWifiP2pServerContent( activity: Activity, - bwfManager: BwfManager, + bwdManager: BwdManager, client: MutableState ) { + /* val expandedStudentList = remember { mutableStateOf(false) } val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) } @@ -90,17 +76,30 @@ fun CommunicationWifiP2pServerContent( "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 - Log.i("room-server", "creating the server") - val server = TaskServer(serverPort.intValue, database) + val server = TaskServer(serverPort.intValue, database, adminPersonEntity) server.start() // Get the client from the server - client.value = server.getClientAdmin() + client.value = server.getAdminClient() } }) { Text("Create") } } + */ } \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/scan/qr/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/scan/qr/screen.kt new file mode 100644 index 0000000..f8cd2da --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/scan/qr/screen.kt @@ -0,0 +1,37 @@ +package com.faraphel.tasks_valider.ui.screen.scan.qr + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.google.zxing.BarcodeFormat +import com.journeyapps.barcodescanner.BarcodeResult +import com.journeyapps.barcodescanner.DecoratedBarcodeView +import com.journeyapps.barcodescanner.DefaultDecoderFactory + +/** + * Screen to scan a Barcode / QR Code + */ +@Composable +fun ScanBarcodeScreen(activity: Activity, barcode: MutableState) { + Box(modifier = Modifier.fillMaxSize()) { + // check and prompt for the camera permission + if (activity.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) + // FIXME(Faraphel): seem to crash the application + activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), 1) + + // AndroidView is used because "DecoratedBarcodeView" only support the legacy view system + AndroidView(factory = { + DecoratedBarcodeView(activity).apply { + this.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) + this.initializeFromIntent(activity.intent) + this.decodeContinuous { result -> barcode.value = result } + this.resume() + } + }) + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/quick_validation.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/quick_validation.kt new file mode 100644 index 0000000..98eff5e --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/quick_validation.kt @@ -0,0 +1,128 @@ +package com.faraphel.tasks_valider.ui.screen.task + +import android.app.Activity +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.navigation.NavController +import com.faraphel.tasks_valider.connectivity.task.TaskClient +import com.faraphel.tasks_valider.database.entities.PersonEntity +import com.faraphel.tasks_valider.database.entities.ValidationEntity +import com.faraphel.tasks_valider.ui.screen.scan.qr.ScanBarcodeScreen +import com.journeyapps.barcodescanner.BarcodeResult +import okhttp3.HttpUrl.Companion.toHttpUrl + + +@Composable +fun QuickValidationScreen( + controller: NavController, + activity: Activity, + client: TaskClient, + user: PersonEntity, +) { + val barcode = remember { mutableStateOf(null) } + + // prompt for the qr code if not found + if (barcode.value == null) + return ScanBarcodeScreen(activity, barcode) + + // show the content of the qr code + val studentUrl = barcode.value!!.text.toHttpUrl() + val cardId = studentUrl.pathSegments[0] + + // when the barcode changed + LaunchedEffect(cardId) { + Thread { + quickValidation( + controller, + activity, + client, + user, + cardId, + ) + }.start() + } +} + + +/** + * Validate the latest task of a user from its student card + */ +fun quickValidation( + controller: NavController, + activity: Activity, + client: TaskClient, + user: PersonEntity, + cardId: String, +) { + // action when an error occurred or everything worked + fun finish() { + activity.runOnUiThread { + // go back to the main screen + controller.navigateUp() + } + } + + // requests all the persons + val allPersons = client.personApi.getAll() + // get the person with the matching card + val person = allPersons.firstOrNull { person -> person.cardId == cardId } + + if (person == null) { + // tell to the user that this card is linked to nobody + activity.runOnUiThread { + Toast.makeText(activity, "No person found for that card.", Toast.LENGTH_LONG).show() + } + return finish() + } + + // requests all the relation persons - subjects + val allRelationsPersonSubject = client.relationPersonSessionSubjectApi.getAll() + // get the corresponding relation + val relationPersonSubject = allRelationsPersonSubject.first { relation -> relation.studentId == person.id } + + // requests all the tasks + val allTasks = client.taskApi.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() + // get the corresponding relation + val validations = allValidations.filter { validation -> validation.studentId == person.id } + + // get the first task without any validation + val task = tasks.firstOrNull { task -> + // check in all the validations if the task is found + val validation = validations.firstOrNull { validation -> validation.taskId == task.id } + // keep the task if it has no validation + validation == null + } + + if (task == null) { + // tell to the user the action cannot be done + activity.runOnUiThread { + Toast.makeText(activity, "There are no tasks left.", Toast.LENGTH_LONG).show() + } + return finish() + } + + // create a new validation on the server + client.validationApi.save( + ValidationEntity( + teacherId=user.id, + studentId=person.id, + taskId=task.id, + ) + ) + // confirm to the user the action was successful + activity.runOnUiThread { + Toast.makeText(activity, "Validated \"${task.title}\".", Toast.LENGTH_LONG).show() + } + + return finish() +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/screen.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/screen.kt deleted file mode 100644 index b14c204..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/screen.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.faraphel.tasks_valider.ui.screen.task - -import android.app.Activity -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import com.faraphel.tasks_valider.connectivity.task.TaskClient - - -/** - * This screen represent a session - * @param activity the android activity - * @param client an HTTP client that can communicate with the server - */ -@Composable -fun TaskSessionScreen(activity: Activity, client: TaskClient) { - Text("WIP : Session Screen") - - /* - val students = remember { mutableStateOf?>(null) } - - // title - Text(text = "Task Group") - - // if the groups are not yet defined, refresh the list - if (groups.value == null) { - Thread { refreshGroups(activity, client, groups) }.start() - return - } - - // if the groups have already been defined, display them - for (group in groups.value!!) { - Text(text = group.toString()) - } - */ -} - - -/* -fun refreshGroups(activity: Activity, client: TaskClient, groups: MutableState?>) { - // try to obtain the list of groups - val response = client.get("entities/group") - - // in case of error, notify it - if (!response.isSuccessful) { - Toast.makeText(activity, response.message, Toast.LENGTH_LONG).show() - return - } - - // parse the list of groups - groups.value = jsonParser.fromJson( - response.body.toString(), - object : TypeToken>(){} - ) -} -*/ \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/session.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/session.kt new file mode 100644 index 0000000..4b2805c --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/session.kt @@ -0,0 +1,189 @@ +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 +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.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.TaskRole +import com.faraphel.tasks_valider.database.entities.PersonEntity +import com.faraphel.tasks_valider.utils.parser +import java.io.File + + +@Composable +fun TaskSessionController( + activity: Activity, + client: TaskClient, + user: PersonEntity, +) { + val controller = rememberNavController() + + NavHost( + navController = controller, + startDestination = "main" + ) { + composable("main") { + TaskSessionScreen(controller, activity, client, user) + } + composable("quick_validation") { + QuickValidationScreen(controller, activity, client, user) + } + } +} + + +/** + * This screen represent a session + * @param activity the android activity + * @param client an HTTP client that can communicate with the server + */ +@Composable +fun TaskSessionScreen( + controller: NavController, + activity: Activity, + client: TaskClient, + user: PersonEntity, +) { + val students = remember { mutableStateOf?>(null) } + val selectedStudent = remember { mutableStateOf(null) } + + // if the groups are not yet defined, refresh the list + if (students.value == null) + return LaunchedEffect(true) { + Thread { refreshStudents(activity, client, students) }.start() + } + + if (selectedStudent.value != null) + return TaskStudentScreen( + activity, + client, + user, + selectedStudent.value!! + ) + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // title + Text( + text = "Session", + fontSize = 32.sp + ) + + // separator + Spacer(modifier = Modifier.height(24.dp)) + + // if the groups have already been defined, display them + for (student in students.value!!) { + Button( + modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp, horizontal = 16.dp), + onClick = { selectedStudent.value = student }, + ) { + Text(text = student.fullName()) + } + } + + // separator + Spacer(modifier = Modifier.weight(1f)) + + // buttons + Row { + // quick validation + Button(onClick = { controller.navigate("quick_validation") }) { + Text("Quick Validation") + } + Button(onClick = { Thread { exportToFile(activity, client) }.start() }) { + Text("Export") + } + } + } +} + + +fun refreshStudents( + activity: Activity, + client: TaskClient, + students: MutableState?> +) { + try { + // try to get all the persons in that session + students.value = client.personApi.getAll() + } catch (exception: Exception) { + // in case of error, show a message + return activity.runOnUiThread { + Log.e("students", "$exception") + Toast.makeText(activity, "Could not retrieve students.\n\n$exception", Toast.LENGTH_LONG).show() + } + } +} + + +fun exportToFile( + activity: Activity, + client: TaskClient, +) { + // get all the values to export + val allPersons = client.personApi.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() + + // for each student + + val data = allStudents.map { student -> + // fetch their subject + val relationStudentSessionObject = allRelationsStudentSessionSubject.first { relation -> + relation.studentId == student.id + } + val subject = allSubjects.first { subject -> subject.id == relationStudentSessionObject.subjectId } + // get the tasks of this person + val tasks = allTasks.filter { task -> task.subjectId == subject.id } + + // for all the tasks + val studentTasksData = tasks.map { task -> + // if a validation match + val isValidated = allValidations.any { validation -> validation.taskId == task.id } + // return the pair of the task title and its validation + mapOf( + "name" to task.title, + "validated" to isValidated, + ) + } + + // return the name of the student associated to his data + mapOf( + "name" to student.fullName(), + "subject" to subject.name, + "tasks" to studentTasksData, + ) + } + + val dataJson = parser.toJson(data) + Log.i("export", dataJson) + + // write all the data in a json file + val file = File(activity.filesDir, "test.json") + file.writeText(dataJson) + + // TODO(Faraphel): prompt to open the file ? +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/student.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/student.kt new file mode 100644 index 0000000..61af054 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/student.kt @@ -0,0 +1,163 @@ +package com.faraphel.tasks_valider.ui.screen.task + +import android.app.Activity +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.faraphel.tasks_valider.connectivity.task.TaskClient +import com.faraphel.tasks_valider.connectivity.task.session.TaskPermission +import com.faraphel.tasks_valider.database.entities.* +import com.faraphel.tasks_valider.utils.dateTimeFormatter +import com.faraphel.tasks_valider.utils.parser +import com.google.gson.reflect.TypeToken + + +/** + * This screen represent a student + * @param student the student object + */ +@Composable +fun TaskStudentScreen( + activity: Activity, + client: TaskClient, + user: PersonEntity, + student: PersonEntity, +) { + val tasks = remember { mutableStateOf?>(null) } + val validations = remember { mutableStateOf?>(null) } + + if (tasks.value == null || validations.value == null) + Thread { + refreshTasksValidations( + activity, + client, + student, + tasks, + validations + ) + }.start() + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // title + Text(text = "Student", fontSize = 32.sp) + // student name - subtitle + Text(text = student.fullName(), fontSize = 24.sp) + + // separator + Spacer(modifier = Modifier.height(24.dp)) + + // if both the list of tasks and validations are loaded + if (!(tasks.value == null || validations.value == null)) { + tasks.value?.forEach { task -> + // get the validation + val validation = validations.value!!.firstOrNull { validation -> validation.taskId == task.id } + + Box( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 64.dp), + ) { + Row { + Column { + // task title + Text(text = task.title, fontWeight = FontWeight.Bold) + // task description + task.description?.let { description -> Text(description) } + // if the task have been validated, show the date + if (validation != null) Text(text = dateTimeFormatter.format(validation.date)) + } + + // separator + Spacer(modifier = Modifier.fillMaxWidth()) + + // the validation state + Checkbox( + checked = validation != null, + enabled = user.role.permissions.contains(TaskPermission.WRITE), + onCheckedChange = { state -> + Thread { + // TODO(Faraphel): simplify or put the UI refresh in the update function ? + + // send a notification to the server about the validation + updateValidation( + client, + state, + validation ?: ValidationEntity( + teacherId=user.id, + studentId=student.id, + taskId=task.id, + ) + ) + // refresh the UI + refreshTasksValidations( + activity, + client, + student, + tasks, + validations + ) + }.start() + } + ) + } + } + } + } + } +} + +fun refreshTasksValidations( + activity: Activity, + client: TaskClient, + student: PersonEntity, + tasks: MutableState?>, + validations: MutableState?>, +) { + // try to obtain the list of subject + val allRelationsPersonSessionSubject = client.relationPersonSessionSubjectApi.getAll() + // get the subject that the student is using + val relationPersonSessionSubject = allRelationsPersonSessionSubject.firstOrNull { relation -> + relation.studentId == student.id + } + + if (relationPersonSessionSubject == null) + // TODO(Faraphel): should be able to assign a subject ? + 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() + + // get the tasks that are linked to this subject + tasks.value = allTasks + .filter { task -> task.subjectId == relationPersonSessionSubject.subjectId } + .sortedBy { task -> task.order } + + // try to obtain the list of validations + val allValidations = client.validationApi.getAll() + // filter only the interesting validations + validations.value = allValidations.filter { validation -> + validation.studentId == student.id && + validation.taskId in allTasks.map { task -> task.id } + } +} + + +fun updateValidation(client: TaskClient, checked: Boolean, validation: ValidationEntity) { + if (checked) + // if the validation is not set, create it + client.validationApi.save(validation) + else + // if the validation is set, delete it + client.validationApi.delete(validation) +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/utils/converters/InstantConverter.kt b/app/src/main/java/com/faraphel/tasks_valider/utils/converters/InstantConverter.kt new file mode 100644 index 0000000..9b317d5 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/utils/converters/InstantConverter.kt @@ -0,0 +1,48 @@ +package com.faraphel.tasks_valider.utils.converters + +import androidx.room.TypeConverter +import com.google.gson.* +import java.lang.reflect.Type +import java.time.Instant + + +/** + * Allow for automatically converting Instant values when using them in the database or in a json parser. + */ +class InstantConverter : JsonDeserializer, JsonSerializer { + /** + * Convert a long into an instant + * @param value the number of milliseconds since the epoch of the time + * @return the Instant object + */ + @TypeConverter + fun deserialize(value: Long): Instant { + return Instant.ofEpochMilli(value) + } + + /** + * Convert a long into an instant + * @param instant the Instant object + * @return the number of milliseconds since the epoch of the time + */ + @TypeConverter + fun serialize(instant: Instant): Long { + return instant.toEpochMilli() + } + + /** + * Convert a long into an instant + * @param json the json object + * @return the Instant object + */ + override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): Instant = + this.deserialize(json.asLong) + + /** + * Convert a long into an instant + * @param instant the instant object + * @return the json object + */ + override fun serialize(instant: Instant, type: Type, context: JsonSerializationContext): JsonElement = + JsonPrimitive(this.serialize(instant)) +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/utils/json.kt b/app/src/main/java/com/faraphel/tasks_valider/utils/json.kt new file mode 100644 index 0000000..5612d11 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/utils/json.kt @@ -0,0 +1,11 @@ +package com.faraphel.tasks_valider.utils + +import com.faraphel.tasks_valider.utils.converters.InstantConverter +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import java.time.Instant + + +val parser: Gson = GsonBuilder() + .registerTypeAdapter(Instant::class.java, InstantConverter()) + .create() \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/utils/requests.kt b/app/src/main/java/com/faraphel/tasks_valider/utils/requests.kt new file mode 100644 index 0000000..71f6fe9 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/utils/requests.kt @@ -0,0 +1,22 @@ +package com.faraphel.tasks_valider.utils + +import fi.iki.elonen.NanoHTTPD +import java.nio.charset.Charset + + +/** + * Return the body of a request as a string. + * :param charset: the encoding of the body + */ +fun NanoHTTPD.IHTTPSession.getBody( + charset: Charset = Charset.forName("UTF-8") +): String { + // get the length of the body + val length = this.headers["content-length"]!!.toInt() + // prepare a buffer for the body + val buffer = ByteArray(length) + // read the body into the buffer + this.inputStream.read(buffer, 0, length) + // convert that buffer into a string + return buffer.toString(charset) +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/utils/time.kt b/app/src/main/java/com/faraphel/tasks_valider/utils/time.kt new file mode 100644 index 0000000..e985688 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/utils/time.kt @@ -0,0 +1,8 @@ +package com.faraphel.tasks_valider.utils + +import java.time.ZoneId +import java.time.format.DateTimeFormatter + + +var dateTimeFormatter: DateTimeFormatter = + DateTimeFormatter.ofPattern("yyyy/MM/dd hh:mm:ss").withZone(ZoneId.systemDefault())