diff --git a/.gitignore b/.gitignore index aa724b7..faf530b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ *.iml .gradle /local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +/.idea/ .DS_Store /build /captures diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 01f6c76..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -tasks-valider \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml deleted file mode 100644 index 6bbe2ae..0000000 --- a/.idea/appInsightsSettings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 0c0c338..0000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 0897082..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 44ca2d9..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index fdf8d99..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6..0000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 8978d23..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 998a95c..2568df0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,8 @@ plugins { id("com.android.application") - id("org.jetbrains.kotlin.android") id("com.google.devtools.ksp") + kotlin("android") + kotlin("plugin.serialization") } android { @@ -41,7 +42,7 @@ android { compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.5.1" + kotlinCompilerExtensionVersion = "1.5.13" } packaging { resources { @@ -51,15 +52,20 @@ android { } dependencies { - implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.core:core-ktx:1.13.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") - implementation("androidx.activity:activity-compose:1.8.2") + implementation("androidx.activity:activity-compose:1.9.0") implementation(platform("androidx.compose:compose-bom:2023.08.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") implementation("androidx.room:room-ktx:2.6.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation("androidx.navigation:navigation-compose:2.7.7") + implementation("org.nanohttpd:nanohttpd:2.3.1") + implementation("com.google.code.gson:gson:2.10.1") + implementation("com.squareup.okhttp3:okhttp-android:5.0.0-alpha.14") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e31d521..051a946 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,33 @@ + + + + + + + + + + + + + + + + + + + + - WidgetTaskStudent(database, taskGroup) - } - } + CommunicationScreen(this) } + } - */ + @SuppressLint("UnspecifiedRegisterReceiverFlag") + override fun onResume() { + super.onResume() + // enable the WiFi-Direct events + this.registerReceiver(this.bwfManager, BwfManager.ALL_INTENT_FILTER) + } + + override fun onPause() { + super.onPause() + + // disable the WiFi-Direct events + this.unregisterReceiver(this.bwfManager) } } 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/bwf/BwfManager.kt new file mode 100644 index 0000000..31f6681 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/BwfManager.kt @@ -0,0 +1,192 @@ +package com.faraphel.tasks_valider.connectivity.bwf + +import android.Manifest +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +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.* + + +/** + * A helper to wrap the WiFi-Direct manager, the channel and the events. + * + * This avoids certain annoying features such as always specifying the channel as the first argument or + * handling all the events with the base event system. + * + * @param manager The WiFi-Direct manager + * @param channel The WiFi-Direct channel + */ +class BwfManager( + private var manager: WifiP2pManager, + private var channel: WifiP2pManager.Channel, +) : BroadcastReceiver() { + companion object { + var PERMISSION_ACCESS_FINE_LOCATION = 1001 ///< permission code for the fine location + var ALL_INTENT_FILTER = IntentFilter().apply { + addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) + } + + fun isSupported(context: Context): Boolean { + return context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT) + } + + /** + * Create a new BwfManager from an activity. + * @param activity The activity to create the manager from + */ + fun fromActivity(activity: Activity): BwfManager { + // 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() + } + + // TODO(Faraphel): more check on permissions + if ( + activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || + activity.checkSelfPermission(Manifest.permission.NEARBY_WIFI_DEVICES) != PackageManager.PERMISSION_GRANTED + ) { + // TODO(Faraphel): should be used with shouldShowRequestPermissionRationale, with a check + activity.requestPermissions( + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + PERMISSION_ACCESS_FINE_LOCATION + ) + } + + // get the WiFi-Direct manager + val manager = activity.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager? + ?: throw BwfPermissionException() + + // get the WiFi-Direct channel + val channel = manager.initialize(activity, activity.mainLooper, null) + return BwfManager(manager, channel) + + // NOTE(Faraphel): the broadcast receiver should be registered in the activity onResume + } + } + + // Wrappers + + /** + * Connect to another device, allowing for a communication using Sockets + * @see WifiP2pManager.connect + * @throws SecurityException if the permission has not been given + */ + @Throws(SecurityException::class) + 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) + }) + + /** + * Request the list of peers after a discovery. + * @see WifiP2pManager.requestPeers + * @throws SecurityException if the permission has not been given + */ + @Throws(SecurityException::class) + fun requestPeers(callback: (WifiP2pDeviceList) -> Unit = {}) = + this.manager.requestPeers(this.channel, callback) + + /** + * Start discovering peers. + * Once founds, the WIFI_P2P_PEERS_CHANGED_ACTION event will be triggered. + * @see WifiP2pManager.discoverPeers + * @throws SecurityException if the permission has not been given + */ + @Throws(SecurityException::class) + fun discoverPeers(callback: () -> Unit = {}) = + this.manager.discoverPeers(this.channel, object : WifiP2pManager.ActionListener { + override fun onSuccess() { callback() } + override fun onFailure(reason: Int) = throw BwfDiscoverException(reason) + }) + + /** + * Obtain information about a connection with another device. + * @see WifiP2pManager.requestConnectionInfo + */ + fun requestConnectionInfo(callback: (WifiP2pInfo) -> Unit = {}) = + this.manager.requestConnectionInfo(this.channel, callback) + + /** + * Obtain information about the current group. + * @see WifiP2pManager.requestGroupInfo + * @throws SecurityException if the permission has not been given + */ + @Throws(SecurityException::class) + fun requestGroupInfo(callback: (WifiP2pGroup?) -> Unit = {}) = + this.manager.requestGroupInfo(this.channel, callback) + + /** + * Create a new WiFi-Direct group. + * @see WifiP2pManager.createGroup + * @throws SecurityException if the permission has not been given + */ + @Throws(SecurityException::class) + fun createGroup(callback: () -> Unit = {}) = + this.manager.createGroup(this.channel, object : WifiP2pManager.ActionListener { + override fun onSuccess() { callback() } + override fun onFailure(reason: Int) = throw BwfCreateGroupException(reason) + }) + + /** + * Disconnect from the current WiFi-Direct group. + * @see WifiP2pManager.removeGroup + */ + fun removeGroup(callback: () -> Unit = {}) = + this.manager.removeGroup(this.channel, object : WifiP2pManager.ActionListener { + override fun onSuccess() { callback() } + override fun onFailure(reason: Int) = throw BwfRemoveGroupException(reason) + }) + + /** + * Create a new WiFi-Direct group. If already connected to a group, quit it first. + * + * Note: most of the failure on removal are caused by not having a group already created, which is checked. + * + * @param callback: the createGroup listener + * + * @see WifiP2pManager.createGroup + * @see WifiP2pManager.removeGroup + */ + fun recreateGroup(callback: () -> Unit = {}) { + // get the current group information + this.requestGroupInfo { group -> + // if a group exist, quit it + if (group != null) + this.removeGroup { this@BwfManager.createGroup(callback) } + else + // create the group + this.createGroup(callback) + } + } + + // Events + + val stateConnectionInfo = mutableStateOf(null) + val statePeers = mutableStateOf(null) + + override fun onReceive(context: Context?, intent: Intent?) { + // ignore empty intent + if (intent == null) + return + + // update the action corresponding state + when (intent.action) { + WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> this.requestConnectionInfo { + connectionInfo -> stateConnectionInfo.value = connectionInfo + } + WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> this.requestPeers { + peers -> statePeers.value = peers + } + } + // TODO(Faraphel): implement event dispatcher + } +} \ No newline at end of file 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/bwf/README.md new file mode 100644 index 0000000..4e0acc5 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/README.md @@ -0,0 +1,11 @@ +# Better WiFi-Direct (BWD) + +This package contain code to improve the base WiFi-Direct implementation. + +The base have some issue, like an abusive usage of listener, error code and events that make using it +very impractical. + +This improved version will instead focus on asynchronous function and exception, allowing for a +cleaner linear code instead. + +(Author: https://git.faraphel.fr/Faraphel) 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 new file mode 100644 index 0000000..a581c09 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfConnectException.kt @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..0ce1169 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfCreateGroupException.kt @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..a4e818a --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfDiscoverException.kt @@ -0,0 +1,5 @@ +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/BwfException.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfException.kt new file mode 100644 index 0000000..7fd8810 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfException.kt @@ -0,0 +1,9 @@ +package com.faraphel.tasks_valider.connectivity.bwf.error + + +/** + * Base Exception for everything concerning the WifiP2pHelper class + */ +open class BwfException( + override val message: String? +) : Exception(message) 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 new file mode 100644 index 0000000..0b690a3 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfInvalidActionException.kt @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..3585496 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfNotSupportedException.kt @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..81c85ee --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfPermissionException.kt @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..92d0789 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/bwf/error/BwfRemoveGroupException.kt @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..b1917e1 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskClient.kt @@ -0,0 +1,100 @@ +package com.faraphel.tasks_valider.connectivity.task + +import okhttp3.HttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody + + +/** + * A client to handle the room connection. + * @param address the address of the server + * @param port the port of the server + * @param baseCookies list of cookies to use (optional) + */ +class TaskClient( + private val address: String, + 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 + + override fun loadForRequest(url: HttpUrl): List { + return this.cookies + } + override fun saveFromResponse(url: HttpUrl, cookies: List) { + this.cookies.addAll(cookies) + } + } + ).build() + + // 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() +} \ 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 new file mode 100644 index 0000000..ba2151a --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/TaskServer.kt @@ -0,0 +1,94 @@ +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 fi.iki.elonen.NanoHTTPD + + +/** + * A server to handle the task API to allow clients to interact with the database. + * @param port the port of the server + * @param database the database to interact with + */ +class TaskServer( + private val port: Int, + private val database: TaskDatabase +) : NanoHTTPD(port) { + companion object { + private val TASK_SESSION_ADMIN = TaskSession( ///< the admin default session + role = TaskRole.ADMIN + ) + } + + private val sessionManager = TaskSessionManager() ///< the session manager + private val adminSessionId = this.sessionManager.newSessionData(TASK_SESSION_ADMIN) ///< default admin session id + + private val sessionManagerApi = TaskSessionManagerApi(this.sessionManager) ///< the api of the session manager + private val databaseApi = TaskDatabaseApi(this.database) ///< the api of the database + + /** + * Return a new client that can be used by the admin + */ + fun getClientAdmin(): TaskClient { + // create the session cookie for the admin + val cookieSession = okhttp3.Cookie.Builder() + .domain("localhost") + .name("sessionId") + .value(adminSessionId) + .build() + // create a new client + return TaskClient( + "localhost", + this.port, + listOf(cookieSession) + ) + } + + /** + * Handle an API request + * @param httpSession the http session + */ + override fun serve(httpSession: IHTTPSession): Response { + // get the session data of the client + val taskSession = this.sessionManager.getOrCreateSessionData(httpSession) + + // parse the url + val uri: String = httpSession.uri.trim('/') + val path = uri.split("/").toMutableList() + + // get the type of the request from the uri + val requestType = path.removeFirstOrNull() + ?: return newFixedLengthResponse( + NanoHTTPD.Response.Status.BAD_REQUEST, + "text/plain", + "Missing request type" + ) + + // get the response from the correct part of the application + val response = when (requestType) { + // session requests + "sessions" -> this.sessionManagerApi.handleRequest(taskSession, httpSession, path) + // entities requests + "entities" -> return this.databaseApi.handleRequest(taskSession, httpSession, path) + // invalid requests + else -> + newFixedLengthResponse( + NanoHTTPD.Response.Status.BAD_REQUEST, + "text/plain", + "Unknown request type" + ) + } + + // wrap additional information in the response + return this.sessionManager.responseSetSessionData(response, httpSession.cookies) + } + + /** + * Start the server with the default configuration + */ + override fun start() = super.start(SOCKET_READ_TIMEOUT, false) +} 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 new file mode 100644 index 0000000..305e3f3 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/api/TaskSessionManagerApi.kt @@ -0,0 +1,139 @@ +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.google.gson.reflect.TypeToken +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 + + /** + * Handle a HTTP Api request + * @param taskSession the data of the client session + * @param httpSession the data of the http session + * @param path the path of the request + */ + fun handleRequest( + taskSession: TaskSession, + httpSession: NanoHTTPD.IHTTPSession, + path: MutableList, + ): NanoHTTPD.Response { + // get the target session id + val targetSessionId = 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) + } + } + + /** + * Handle a request with no specific session targeted + */ + private fun handleRequestGeneric( + taskSession: TaskSession, + httpSession: NanoHTTPD.IHTTPSession, + ): NanoHTTPD.Response { + when (httpSession.method) { + // get all the session data + NanoHTTPD.Method.GET -> { + // check the permission of the session + if (taskSession.role.permissions.contains(TaskPermission.READ)) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.FORBIDDEN, + "text/plain", + "Forbidden" + ) + + // return the session data + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.OK, + "application/json", + jsonParser.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 + NanoHTTPD.Method.POST -> { + // 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 update a session" + ) + + // parse the content of the request + val targetSession = jsonParser.fromJson( + httpSession.inputStream.bufferedReader().readText(), + TaskSession::class.java + ) + + // update the session + this.sessionManager.setSessionData(targetSessionId, targetSession) + + // success message + return 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" + ) + } + // ignore other methods + else -> { + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, + "text/plain", + "Invalid method" + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskPermission.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskPermission.kt new file mode 100644 index 0000000..74d1809 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskPermission.kt @@ -0,0 +1,7 @@ +package com.faraphel.tasks_valider.connectivity.task.session + +enum class TaskPermission { + READ, + WRITE, + ADMIN +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskRole.kt b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskRole.kt new file mode 100644 index 0000000..cb519be --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskRole.kt @@ -0,0 +1,31 @@ +package com.faraphel.tasks_valider.connectivity.task.session + + +/** + * A role system that can be used for in the task system + */ +enum class TaskRole(val value: String) { + NONE("none") { + override var permissions: List = listOf() + }, + STUDENT("student") { + override var permissions = listOf( + TaskPermission.READ + ) + }, + TEACHER("teacher") { + override var permissions: List = listOf( + TaskPermission.READ, + TaskPermission.WRITE + ) + }, + ADMIN("admin") { + override var permissions: List = listOf( + TaskPermission.READ, + TaskPermission.WRITE, + TaskPermission.ADMIN + ) + }; + + abstract var permissions: List +} 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 new file mode 100644 index 0000000..84c4c37 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSession.kt @@ -0,0 +1,13 @@ +package com.faraphel.tasks_valider.connectivity.task.session + +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 +) 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 new file mode 100644 index 0000000..e0552c6 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/connectivity/task/session/TaskSessionManager.kt @@ -0,0 +1,100 @@ +package com.faraphel.tasks_valider.connectivity.task.session + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import fi.iki.elonen.NanoHTTPD +import java.util.* + + +/** + * The manager for the session system + */ +class TaskSessionManager { + private val sessions = mutableMapOf() ///< sessions specific data + + /** + * 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 + */ + fun newSessionData( + session: TaskSession = TaskSession(), + sessionId: String = UUID.randomUUID().toString() + ): String { + this.sessions[sessionId] = session + return sessionId + } + + /** + * Get data from a http session + * @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) + return sessionData + } + + /** + * Get data from a session identifier + * @param sessionId the identifier of the session + * @return the session data + */ + fun getSessionData(sessionId: String): TaskSession? { + return this.sessions[sessionId] + } + + /** + * Set the data of a session + * @param sessionId the identifier of the session + * @param session the session data + */ + fun setSessionData(sessionId: String, session: TaskSession) { + this.sessions[sessionId] = session + } + + /** + * Delete a session + * @param sessionId the identifier of the session + */ + fun deleteSessionData(sessionId: String): TaskSession? { + return this.sessions.remove(sessionId) + } + + /** + * Get data from a http session. If it does not exist, create it. + * @param httpSession the HTTP session + */ + fun getOrCreateSessionData(httpSession: NanoHTTPD.IHTTPSession): TaskSession { + // 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 + } + + /** + * Insert new cookies for a session in a response + * @param response the response to inject cookies into + * @param cookies the cookie handler + */ + fun responseSetSessionData( + response: NanoHTTPD.Response, + cookies: NanoHTTPD.CookieHandler + ): NanoHTTPD.Response { + // update the cookie of the user + cookies.set(NanoHTTPD.Cookie("sessionId", this.newSessionData())) + // load them in the response + cookies.unloadQueue(response) + + return response + } +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/Database.kt b/app/src/main/java/com/faraphel/tasks_valider/database/Database.kt deleted file mode 100644 index 17222f1..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/Database.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.faraphel.tasks_valider.database - -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.room.Database -import androidx.room.DatabaseConfiguration -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import com.faraphel.tasks_valider.database.converters.InstantConverter -import com.faraphel.tasks_valider.database.dao.GroupDao -import com.faraphel.tasks_valider.database.dao.GroupStudentDao -import com.faraphel.tasks_valider.database.dao.StudentDao -import com.faraphel.tasks_valider.database.dao.TaskDao -import com.faraphel.tasks_valider.database.dao.TaskGroupDao -import com.faraphel.tasks_valider.database.dao.TeacherDao -import com.faraphel.tasks_valider.database.entities.Group -import com.faraphel.tasks_valider.database.entities.GroupStudent -import com.faraphel.tasks_valider.database.entities.Student -import com.faraphel.tasks_valider.database.entities.Task -import com.faraphel.tasks_valider.database.entities.TaskGroup -import com.faraphel.tasks_valider.database.entities.Teacher -import java.time.Instant - - -@Database( - entities = [ - Group::class, - Student::class, - Teacher::class, - Task::class, - - GroupStudent::class, - TaskGroup::class, - ], - version = 1 -) -@TypeConverters( - InstantConverter::class -) -abstract class Database : RoomDatabase() { - // entities - abstract fun groupDao(): GroupDao - abstract fun studentDao(): StudentDao - abstract fun teacherDao(): TeacherDao - abstract fun taskDao(): TaskDao - - // relations - abstract fun groupStudentDao(): GroupStudentDao - abstract fun taskGroupDao(): TaskGroupDao -} 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 new file mode 100644 index 0000000..f2ff03e --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/TaskDatabase.kt @@ -0,0 +1,40 @@ +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.database.dao.* +import com.faraphel.tasks_valider.database.entities.* + + +/** + * The database for the tasks' application. + * Contains the entities and the relations between them. + */ +@Database( + entities = [ + ClassEntity::class, + PersonEntity::class, + SessionEntity::class, + SubjectEntity::class, + TaskEntity::class, + ValidationEntity::class, + + RelationClassPersonEntity::class, + ], + version = 1 +) +@TypeConverters( + InstantConverter::class +) +abstract class TaskDatabase: RoomDatabase() { + abstract fun classDao(): ClassDao + abstract fun personDao(): PersonDao + abstract fun sessionDao(): SessionDao + abstract fun subjectDao(): SubjectDao + abstract fun taskDao(): TaskDao + abstract fun validationDao(): ValidationDao + + abstract fun relationClassPersonDao(): RelationClassPersonDao +} 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/TaskDatabaseApi.kt new file mode 100644 index 0000000..2303a34 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/TaskDatabaseApi.kt @@ -0,0 +1,110 @@ +package com.faraphel.tasks_valider.database.api + +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.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()), + ) + + /** + * handle an API request + * @param taskSession the current user session + * @param httpSession the http session + * @param path the path of the request + */ + fun handleRequest( + taskSession: TaskSession, + httpSession: NanoHTTPD.IHTTPSession, + path: MutableList + ): NanoHTTPD.Response { + // get the entity name + val entityName = path.removeFirstOrNull() + ?: return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.BAD_REQUEST, + "text/plain", + "Missing entity name" + ) + + // get the correspond Api object for this entity + val entityApi = this.api[entityName] + ?: return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.NOT_FOUND, + "text/plain", + "Unknown entity name" + ) + + // dispatch the request to the correct entity API + when (httpSession.method) { + // check if the data is in the database + // 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)) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.FORBIDDEN, + "text/plain", + "Forbidden" + ) + + return entityApi.head(httpSession) + } + // get the data from the database + NanoHTTPD.Method.GET -> { + // check the permission of the session + if (taskSession.role.permissions.contains(TaskPermission.READ)) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.FORBIDDEN, + "text/plain", + "Forbidden" + ) + + return entityApi.get(httpSession) + } + // insert the data into the database + NanoHTTPD.Method.POST -> { + // check the permission of the session + if (taskSession.role.permissions.contains(TaskPermission.WRITE)) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.FORBIDDEN, + "text/plain", + "Forbidden" + ) + + return entityApi.post(httpSession) + } + // delete the data from the database + NanoHTTPD.Method.DELETE -> { + // check the permission of the session + if (taskSession.role.permissions.contains(TaskPermission.WRITE)) + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.FORBIDDEN, + "text/plain", + "Forbidden" + ) + + return entityApi.delete(httpSession) + } + // other methods are not allowed + else -> + return NanoHTTPD.newFixedLengthResponse( + NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, + "text/plain", + "Method not allowed" + ) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..a012aa2 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/ClassApi.kt @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..d9edf18 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/PersonApi.kt @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..3f437b5 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/RelationClassPersonApi.kt @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..2eb7dc2 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/SessionApi.kt @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..caca63a --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/SubjectApi.kt @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..c331a98 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/TaskApi.kt @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..60e3010 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/ValidationApi.kt @@ -0,0 +1,7 @@ +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/BaseApi.kt b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseApi.kt new file mode 100644 index 0000000..d40665e --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseApi.kt @@ -0,0 +1,32 @@ +package com.faraphel.tasks_valider.database.api.entities.base + +import fi.iki.elonen.NanoHTTPD + +/** + * A base for the API to handle the database operations with an HTTP server. + */ +interface BaseApi { + /** + * Handle the HEAD request + * This is used to check if a data exists in the database + */ + fun head(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response + + /** + * Handle the GET request + * This is used to get data from the database + */ + fun get(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response + + /** + * Handle the POST request + * This is used to insert data into the database + */ + fun post(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response + + /** + * Handle the PUT request + * This is used to delete data from the database + */ + fun delete(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response +} \ No newline at end of file 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 new file mode 100644 index 0000000..4668feb --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/api/entities/base/BaseJsonApi.kt @@ -0,0 +1,72 @@ +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/dao/ClassDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/ClassDao.kt new file mode 100644 index 0000000..baef405 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/ClassDao.kt @@ -0,0 +1,40 @@ +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.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 { + @Query("SELECT * FROM ${ClassEntity.TABLE_NAME}") + override fun getAll(): List + + /** + * Get the object from its identifier + */ + @Query("SELECT * FROM ${ClassEntity.TABLE_NAME} WHERE id = :id") + fun getById(id: Long): ClassEntity + + /** + * Get all the sessions this class attended + * @param id the id of the class + */ + @Query("SELECT * FROM ${SessionEntity.TABLE_NAME} WHERE class_id = :id") + fun getSessions(id: Long): List + + /** + * Get all the students in a class + * @param id the id of the class + */ + @Query( + "SELECT * 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" + ) + fun getStudents(id: Long): List +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/GroupDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/GroupDao.kt deleted file mode 100644 index 33511fd..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/GroupDao.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.faraphel.tasks_valider.database.dao - -import androidx.room.Dao -import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.Group -import com.faraphel.tasks_valider.database.entities.Student - - -@Dao -interface GroupDao : BaseDao { - @Query("SELECT * FROM `groups`") - override fun getAll(): List - - @Query("SELECT * FROM `groups` WHERE id = :id") - fun getById(id: Long): Group - - /** - Allow to get all groups with a specific student - */ - @Query( - "SELECT * FROM `groups` " + - "JOIN `group_student` ON `groups`.id = `group_student`.student_id " + - "WHERE `group_student`.student_id = :studentId" - ) - @RewriteQueriesToDropUnusedColumns - fun filterByStudentId(studentId: Long): List -} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/GroupStudentDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/GroupStudentDao.kt deleted file mode 100644 index 2bd355e..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/GroupStudentDao.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.faraphel.tasks_valider.database.dao - -import androidx.room.Dao -import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.Group -import com.faraphel.tasks_valider.database.entities.GroupStudent -import com.faraphel.tasks_valider.database.entities.Student - - -@Dao -interface GroupStudentDao : BaseDao { - @Query("SELECT * FROM `group_student`") - override fun getAll(): List - - @Query("SELECT * FROM `group_student` WHERE group_id = :groupId AND student_id = :studentId") - fun getById(groupId: Long, studentId: Long): GroupStudent -} 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 new file mode 100644 index 0000000..601a2f6 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/PersonDao.kt @@ -0,0 +1,44 @@ +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.* + +@Dao +interface PersonDao : BaseDao { + @Query("SELECT * FROM ${PersonEntity.TABLE_NAME}") + override fun getAll(): List + + /** + * Get the object from its identifier + */ + @Query("SELECT * FROM ${PersonEntity.TABLE_NAME} WHERE id = :id") + fun getById(id: Long): PersonEntity + + /** + * Allow to get all the classes the person is attending as a student + */ + @Query( + "SELECT * FROM ${ClassEntity.TABLE_NAME} " + + "JOIN ${RelationClassPersonEntity.TABLE_NAME} " + + "ON ${ClassEntity.TABLE_NAME}.id = ${RelationClassPersonEntity.TABLE_NAME}.student_id " + + "WHERE ${RelationClassPersonEntity.TABLE_NAME}.student_id = :id" + ) + fun getClasses(id: Long): List + + + /** + * Get all the tasks this user approved as a teacher + * @param id the id of the person + */ + @Query("SELECT * FROM ${ValidationEntity.TABLE_NAME} WHERE teacher_id = :id") + fun getTasksApproved(id: Long): List + + /** + * Get all the tasks this user validated as a student + * @param id the id of the person + */ + @Query("SELECT * FROM ${ValidationEntity.TABLE_NAME} WHERE student_id = :id") + fun getTasksValidated(id: Long): List +} 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 new file mode 100644 index 0000000..b0296df --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/RelationClassPersonDao.kt @@ -0,0 +1,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.entities.RelationClassPersonEntity + +@Dao +interface RelationClassPersonDao : BaseDao { + @Query("SELECT * FROM ${RelationClassPersonEntity.TABLE_NAME}") + override fun getAll(): List + + /** + * Get the object from its identifiers + */ + @Query( + "SELECT * FROM ${RelationClassPersonEntity.TABLE_NAME} " + + "WHERE " + + "class_id = :classId AND " + + "student_id = :studentId" + ) + fun getById(classId: Long, studentId: Long): RelationClassPersonEntity +} 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 new file mode 100644 index 0000000..8054915 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/SessionDao.kt @@ -0,0 +1,18 @@ +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 + +@Dao +interface SessionDao : BaseDao { + @Query("SELECT * FROM ${SessionEntity.TABLE_NAME}") + override fun getAll(): List + + /** + * Get the object from its identifier + */ + @Query("SELECT * FROM ${SessionEntity.TABLE_NAME} WHERE id = :id") + fun getById(id: Long): SessionEntity +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/StudentDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/StudentDao.kt deleted file mode 100644 index e4b8dfb..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/StudentDao.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.faraphel.tasks_valider.database.dao - -import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns -import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.Group -import com.faraphel.tasks_valider.database.entities.Person -import com.faraphel.tasks_valider.database.entities.Student - - -@Dao -interface StudentDao : BaseDao { - @Query("SELECT * FROM `students`") - override fun getAll(): List - - @Query("SELECT * FROM `students` WHERE id = :id") - fun getById(id: Long): Student - - - /** - Allow to get all the students in a group - */ - @Query( - "SELECT * FROM `students` " + - "JOIN `group_student` ON `students`.id = `group_student`.student_id " + - "WHERE `group_student`.group_id = :groupId" - ) - @RewriteQueriesToDropUnusedColumns - fun filterByGroupId(groupId: Long): List -} 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 new file mode 100644 index 0000000..8dc676a --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/SubjectDao.kt @@ -0,0 +1,27 @@ +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.entities.SubjectEntity +import com.faraphel.tasks_valider.database.entities.TaskEntity + +@Dao +interface SubjectDao : BaseDao { + @Query("SELECT * FROM ${SubjectEntity.TABLE_NAME}") + override fun getAll(): List + + /** + * Get the object from its identifier + */ + @Query("SELECT * FROM ${SubjectEntity.TABLE_NAME} WHERE id = :id") + 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 +} 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 1e2cb8c..0053cd0 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,28 +2,25 @@ package com.faraphel.tasks_valider.database.dao import androidx.room.Dao import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns import com.faraphel.tasks_valider.database.dao.base.BaseDao -import com.faraphel.tasks_valider.database.entities.Group -import com.faraphel.tasks_valider.database.entities.Task - +import com.faraphel.tasks_valider.database.entities.TaskEntity +import com.faraphel.tasks_valider.database.entities.ValidationEntity @Dao -interface TaskDao : BaseDao { - @Query("SELECT * FROM `tasks`") - override fun getAll(): List - - @Query("SELECT * FROM `tasks` WHERE id = :id") - fun getById(id: Long): Task +interface TaskDao : BaseDao { + @Query("SELECT * FROM ${TaskEntity.TABLE_NAME}") + override fun getAll(): List /** - Get all the tasks for a specific group + * Get the object from its identifier */ - @Query( - "SELECT * FROM `tasks` " + - "JOIN `task_group` ON `tasks`.id = `task_group`.task_id " + - "WHERE `task_group`.group_id = :groupId" - ) - @RewriteQueriesToDropUnusedColumns - fun filterByGroupId(groupId: Long): List + @Query("SELECT * FROM ${TaskEntity.TABLE_NAME} WHERE id = :id") + fun getById(id: Long): TaskEntity + + /** + * Get all the validations have been approved for this tasks + * @param id the id of the task + */ + @Query("SELECT * FROM ${ValidationEntity.TABLE_NAME} WHERE task_id = :id") + fun getTasksValidated(id: Long): List } diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/TaskGroupDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/TaskGroupDao.kt deleted file mode 100644 index 0caa673..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/TaskGroupDao.kt +++ /dev/null @@ -1,16 +0,0 @@ -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.TaskGroup - - -@Dao -interface TaskGroupDao : BaseDao { - @Query("SELECT * FROM `task_group`") - override fun getAll(): List - - @Query("SELECT * FROM `task_group` WHERE task_id = :taskId AND group_id = :groupId") - fun getById(taskId: Long, groupId: Long): TaskGroup -} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/dao/TeacherDao.kt b/app/src/main/java/com/faraphel/tasks_valider/database/dao/TeacherDao.kt deleted file mode 100644 index f489725..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/dao/TeacherDao.kt +++ /dev/null @@ -1,18 +0,0 @@ -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.Person -import com.faraphel.tasks_valider.database.entities.Student -import com.faraphel.tasks_valider.database.entities.Teacher - - -@Dao -interface TeacherDao : BaseDao { - @Query("SELECT * FROM `teachers`") - override fun getAll(): List - - @Query("SELECT * FROM `teachers` WHERE id = :id") - fun getById(id: Long): Teacher -} 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 new file mode 100644 index 0000000..544da65 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/dao/ValidationDao.kt @@ -0,0 +1,24 @@ +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.ValidationEntity + +@Dao +interface ValidationDao : BaseDao { + @Query("SELECT * FROM ${ValidationEntity.TABLE_NAME}") + override fun getAll(): List + + /** + * Get the object from its identifiers + */ + @Query( + "SELECT * FROM ${ValidationEntity.TABLE_NAME} " + + "WHERE " + + "teacher_id = :teacherId and " + + "student_id = :studentId and " + + "task_id = :taskId" + ) + 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/BaseDao.kt index c9c4fe2..b06e62f 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/BaseDao.kt @@ -2,18 +2,40 @@ 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. + * @param Entity the entity to handle + */ interface BaseDao { - @Insert + /** + * Check if the entities exists in the database. + */ + fun exists(vararg entities: Entity): Boolean { + return this.getAll().containsAll(entities.toList()) + } + + /** + * Insert the entities into the database. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg entities: Entity): List - @Update - fun update(vararg entities: Entity): Int - + /** + * Delete the entities from the database. + */ @Delete fun delete(vararg entities: Entity): Int + /** + * Get all the entities from the database. + * TODO(Faraphel): support filters ? + */ fun getAll(): List } diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/ClassEntity.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/ClassEntity.kt new file mode 100644 index 0000000..97eb47e --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/ClassEntity.kt @@ -0,0 +1,17 @@ +package com.faraphel.tasks_valider.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.faraphel.tasks_valider.database.entities.base.BaseEntity + + +@Entity(tableName = ClassEntity.TABLE_NAME) +data class ClassEntity ( + @ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo("name") val name: String, +) : BaseEntity() { + companion object { + const val TABLE_NAME = "classes" + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Group.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/Group.kt deleted file mode 100644 index d0f3a09..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Group.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.faraphel.tasks_valider.database.entities - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - - -@Entity(tableName = "groups") -data class Group ( - @ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0, - @ColumnInfo("name") val name: String? = null, -) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Person.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/Person.kt deleted file mode 100644 index 347b05d..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Person.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.faraphel.tasks_valider.database.entities - -import java.util.Locale - - -open class Person ( - open val id: Long = 0, - open val firstName: String, - open val lastName: String, -) { - /** - Get the full name of the person - */ - val fullName: String - get() { - return "${firstName.capitalize(Locale.ROOT)} ${lastName.uppercase()}" - } -} 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 new file mode 100644 index 0000000..2ee3506 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/PersonEntity.kt @@ -0,0 +1,19 @@ +package com.faraphel.tasks_valider.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.faraphel.tasks_valider.database.entities.base.BaseEntity +import java.util.* + +@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, +) : BaseEntity() { + companion object { + const val TABLE_NAME = "persons" + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/GroupStudent.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/RelationClassPersonEntity.kt similarity index 51% rename from app/src/main/java/com/faraphel/tasks_valider/database/entities/GroupStudent.kt rename to app/src/main/java/com/faraphel/tasks_valider/database/entities/RelationClassPersonEntity.kt index 2e830e3..06c024a 100644 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/GroupStudent.kt +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/RelationClassPersonEntity.kt @@ -3,31 +3,34 @@ 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.Group -import com.faraphel.tasks_valider.database.entities.Student +import com.faraphel.tasks_valider.database.entities.base.BaseEntity @Entity( - tableName = "group_student", + tableName = RelationClassPersonEntity.TABLE_NAME, primaryKeys = [ - "group_id", - "student_id" + "student_id", + "class_id", ], foreignKeys = [ ForeignKey( - entity = Group::class, - parentColumns = ["id"], - childColumns = ["group_id"], - onDelete = ForeignKey.CASCADE - ), - ForeignKey( - entity = Student::class, + entity = PersonEntity::class, parentColumns = ["id"], childColumns = ["student_id"], onDelete = ForeignKey.CASCADE - ) + ), + ForeignKey( + entity = ClassEntity::class, + parentColumns = ["id"], + childColumns = ["class_id"], + onDelete = ForeignKey.CASCADE + ), ] ) -data class GroupStudent( - @ColumnInfo("group_id", index = true) val groupId: Long, +data class RelationClassPersonEntity ( @ColumnInfo("student_id", index = true) val studentId: Long, -) + @ColumnInfo("class_id", index = true) val classId: Long, +) : BaseEntity() { + companion object { + const val TABLE_NAME = "relation_class_person" + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/SessionEntity.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/SessionEntity.kt new file mode 100644 index 0000000..0f2edf4 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/SessionEntity.kt @@ -0,0 +1,31 @@ +package com.faraphel.tasks_valider.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.faraphel.tasks_valider.database.entities.base.BaseEntity +import java.time.Instant + +@Entity( + tableName = SessionEntity.TABLE_NAME, + foreignKeys = [ + ForeignKey( + entity = ClassEntity::class, + parentColumns = ["id"], + childColumns = ["class_id"], + onDelete = ForeignKey.CASCADE + ), + ] +) +data class SessionEntity ( + @ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo("name") val name: String? = null, + @ColumnInfo("start") val start: Instant, + + @ColumnInfo("class_id", index = true) val classId: Long? = null, +) : BaseEntity() { + companion object { + const val TABLE_NAME = "sessions" + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Student.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/Student.kt deleted file mode 100644 index 4cff4c6..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Student.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.faraphel.tasks_valider.database.entities - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "students") -class Student( - @ColumnInfo("id") @PrimaryKey(autoGenerate = true) override val id: Long = 0, - @ColumnInfo("first_name") override val firstName: String, - @ColumnInfo("last_name") override val lastName: String -) : Person(id, firstName, lastName) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/SubjectEntity.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/SubjectEntity.kt new file mode 100644 index 0000000..ee8aa83 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/SubjectEntity.kt @@ -0,0 +1,16 @@ +package com.faraphel.tasks_valider.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.faraphel.tasks_valider.database.entities.base.BaseEntity + +@Entity(tableName = SubjectEntity.TABLE_NAME) +data class SubjectEntity ( + @ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo("name") val name: String, +) : BaseEntity() { + companion object { + const val TABLE_NAME = "subjects" + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Task.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/Task.kt deleted file mode 100644 index 15e1361..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Task.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.faraphel.tasks_valider.database.entities - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "tasks") -data class Task ( - @ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0, - @ColumnInfo("title") val title: String, - @ColumnInfo("description") val description: String, -) 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 new file mode 100644 index 0000000..4548c36 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/TaskEntity.kt @@ -0,0 +1,30 @@ +package com.faraphel.tasks_valider.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.faraphel.tasks_valider.database.entities.base.BaseEntity + +@Entity( + tableName = TaskEntity.TABLE_NAME, + foreignKeys = [ + ForeignKey( + entity = SubjectEntity::class, + parentColumns = ["id"], + childColumns = ["subject_id"], + onDelete = ForeignKey.CASCADE + ), + ] +) +data class TaskEntity ( + @ColumnInfo("id") @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo("title") val title: String, + @ColumnInfo("description") val description: String? = null, + + @ColumnInfo("subject_id", index = true) val subjectId: Long, +) : BaseEntity() { + companion object { + const val TABLE_NAME = "tasks" + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/TaskGroup.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/TaskGroup.kt deleted file mode 100644 index 07e9c57..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/TaskGroup.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.faraphel.tasks_valider.database.entities - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import java.time.Instant - - -@Entity( - tableName = "task_group", - primaryKeys = [ - "task_id", - "group_id" - ], - foreignKeys = [ - ForeignKey( - entity = Group::class, - parentColumns = ["id"], - childColumns = ["group_id"], - onDelete = ForeignKey.CASCADE - ), - ForeignKey( - entity = Task::class, - parentColumns = ["id"], - childColumns = ["task_id"], - onDelete = ForeignKey.CASCADE - ), - ForeignKey( - entity = Teacher::class, - parentColumns = ["id"], - childColumns = ["approval_teacher_id"], - onDelete = ForeignKey.CASCADE - ), - ] -) -data class TaskGroup ( - @ColumnInfo("task_id") val taskId: Long, - @ColumnInfo("group_id") val groupId: Long, - @ColumnInfo("approval_status") var approvalStatus: Boolean = false, - @ColumnInfo("approval_teacher_id") val approvalTeacherId: Long? = null, - @ColumnInfo("approval_time") val approvalTime: Instant? = null -) diff --git a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Teacher.kt b/app/src/main/java/com/faraphel/tasks_valider/database/entities/Teacher.kt deleted file mode 100644 index b499973..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/database/entities/Teacher.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.faraphel.tasks_valider.database.entities - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "teachers") -class Teacher( - @ColumnInfo("id") @PrimaryKey(autoGenerate = true) override val id: Long = 0, - @ColumnInfo("first_name") override val firstName: String, - @ColumnInfo("last_name") override val lastName: String -) : Person(id, firstName, lastName) 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 new file mode 100644 index 0000000..467f5df --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/ValidationEntity.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 +import java.time.Instant + +@Entity( + tableName = ValidationEntity.TABLE_NAME, + primaryKeys = [ + "teacher_id", + "student_id", + "task_id", + ], + foreignKeys = [ + ForeignKey( + entity = PersonEntity::class, + parentColumns = ["id"], + childColumns = ["teacher_id"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = PersonEntity::class, + parentColumns = ["id"], + childColumns = ["student_id"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = TaskEntity::class, + parentColumns = ["id"], + childColumns = ["task_id"], + onDelete = ForeignKey.CASCADE + ), + ] +) +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, +) : BaseEntity() { + companion object { + const val TABLE_NAME = "validations" + } +} + 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 new file mode 100644 index 0000000..44654b2 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/database/entities/base/BaseEntity.kt @@ -0,0 +1,3 @@ +package com.faraphel.tasks_valider.database.entities.base + +open class BaseEntity 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/screen.kt new file mode 100644 index 0000000..2cc75b7 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/client/screen.kt @@ -0,0 +1,64 @@ +package com.faraphel.tasks_valider.ui.screen.communication.internet.client + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.text.input.KeyboardType +import com.faraphel.tasks_valider.connectivity.task.TaskClient +import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_ADDRESS +import com.faraphel.tasks_valider.ui.screen.communication.DEFAULT_SERVER_PORT +import com.faraphel.tasks_valider.ui.screen.communication.RANGE_SERVER_PORT +import com.faraphel.tasks_valider.ui.screen.task.TaskSessionScreen + + +@Composable +fun CommunicationInternetClientScreen(activity: Activity) { + val client = remember { mutableStateOf(null) } + + if (client.value == null) CommunicationInternetClientContent(client) + else TaskSessionScreen(activity, client.value!!) +} + + +@Composable +fun CommunicationInternetClientContent(client: MutableState) { + val serverAddress = remember { mutableStateOf(DEFAULT_SERVER_ADDRESS) } + val serverPort = remember { mutableIntStateOf(DEFAULT_SERVER_PORT) } + + Column { + // server address + TextField( + value = serverAddress.value, + onValueChange = { text -> + serverAddress.value = text + } + ) + + // server port + TextField( + value = serverPort.intValue.toString(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + onValueChange = { text -> + val port = text.toInt() + if (port in RANGE_SERVER_PORT) { + serverPort.intValue = port + } + } + ) + + Button(onClick = { + // TODO(Faraphel): check if the server is reachable + client.value = TaskClient(serverAddress.value, serverPort.intValue) + }) { + Text("Connect") + } + } +} \ No newline at end of file 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/screen.kt new file mode 100644 index 0000000..335c2f7 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/screen.kt @@ -0,0 +1,36 @@ +package com.faraphel.tasks_valider.ui.screen.communication.internet + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.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 + + +@Composable +fun CommunicationInternetScreen(activity: Activity) { + val controller = rememberNavController() + + NavHost(navController = controller, startDestination = "mode") { + composable("mode") { CommunicationInternetSelectContent(controller) } + composable("client") { CommunicationInternetClientScreen(activity) } + composable("server") { CommunicationInternetServerScreen(activity) } + } +} + + +@Composable +fun CommunicationInternetSelectContent(controller: NavController) { + Column { + // client mode + Button(onClick = { controller.navigate("client") }) { Text("Client") } + // server mode + Button(onClick = { controller.navigate("server") }) { Text("Server") } + } +} 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 new file mode 100644 index 0000000..cdf8f13 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/internet/server/screen.kt @@ -0,0 +1,99 @@ +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/screen.kt new file mode 100644 index 0000000..bb260ce --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/screen.kt @@ -0,0 +1,77 @@ +package com.faraphel.tasks_valider.ui.screen.communication + +import android.app.Activity +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +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.ui.screen.communication.wifiP2p.CommunicationWifiP2pScreen + + +/** + * 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. + */ +@Composable +fun CommunicationScreen(activity: Activity) { + val controller = rememberNavController() + + NavHost( + navController = controller, + startDestination = "select" + ) { + composable("select") { + CommunicationSelectContent(controller, activity) + } + composable("internet") { + CommunicationInternetScreen(activity) + } + composable("wifi-p2p") { + val bwfManager = BwfManager.fromActivity(activity) + CommunicationWifiP2pScreen(activity, bwfManager) + } + } +} + + +/** + * Communication screen that allows the user to choose the communication mode + */ +@Composable +fun CommunicationSelectContent(controller: NavController, activity: Activity) { + val isWifiP2pSupported = BwfManager.isSupported(activity) + + Column { + // internet communication mode + Button(onClick = { controller.navigate("internet") }) { + Text("Internet") + } + + // wifi-direct communication mode + Button( + colors = ButtonDefaults.buttonColors( + // if the WiFi-Direct is not supported, the button is grayed out + containerColor = if (isWifiP2pSupported) Color.Unspecified else Color.Gray + ), + onClick = { + // if the WiFi-Direct is supported, navigate to the WiFi-Direct screen + if (isWifiP2pSupported) controller.navigate("wifi-p2p") + // if the WiFi-Direct is not supported, show a toast message + else Toast.makeText(activity, "WiFi-Direct is not supported on this device", Toast.LENGTH_SHORT).show() + } + ) { + Text("WiFi-Direct") + } + } +} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/settings.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/settings.kt new file mode 100644 index 0000000..e9362dc --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/settings.kt @@ -0,0 +1,6 @@ +package com.faraphel.tasks_valider.ui.screen.communication + + +const val DEFAULT_SERVER_ADDRESS: String = "127.0.0.1" +const val DEFAULT_SERVER_PORT: Int = 9876 +val RANGE_SERVER_PORT: IntRange = 1024..65535 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 new file mode 100644 index 0000000..3ba8cdd --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/client/screen.kt @@ -0,0 +1,57 @@ +package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p.client + +import android.app.Activity +import android.net.wifi.p2p.WifiP2pConfig +import android.net.wifi.p2p.WifiP2pDevice +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.faraphel.tasks_valider.connectivity.bwf.BwfManager +import com.faraphel.tasks_valider.ui.widgets.connectivity.WifiP2pDeviceListWidget + + +@Composable +fun CommunicationWifiP2pClientScreen(activity: Activity, bwfManager: BwfManager) { + val selectedDevice = remember { mutableStateOf(null) } + val isConnected = remember { mutableStateOf(false) } + + // if connected, show the task group screen + if (isConnected.value) { + // TaskGroupScreen(activity, null) + // TODO(Faraphel): finish the connection + return + } + + + // if the device is selected but not connected, try to connect + if (selectedDevice.value != null) { + // TODO(Faraphel): error handling + val config = WifiP2pConfig().apply { + deviceAddress = selectedDevice.value!!.deviceAddress + } + bwfManager.connect(config) { + isConnected.value = true + } + return + } + + // display the list of devices + CommunicationWifiP2pClientContent(bwfManager, selectedDevice) +} + + +@Composable +fun CommunicationWifiP2pClientContent( + bwfManager: BwfManager, + selectedDevice: MutableState +) { + Column { + WifiP2pDeviceListWidget( + peers = bwfManager.statePeers.value, + filter = { device: WifiP2pDevice -> device.isGroupOwner }, + selectedDevice, + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..bd21fef --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/screen.kt @@ -0,0 +1,37 @@ +package com.faraphel.tasks_valider.ui.screen.communication.wifiP2p + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.faraphel.tasks_valider.connectivity.bwf.BwfManager +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) { + val controller = rememberNavController() + + NavHost(navController = controller, startDestination = "mode") { + composable("mode") { CommunicationWifiP2pSelectContent(controller) } + composable("client") { CommunicationWifiP2pClientScreen(activity, bwfManager) } + composable("server") { CommunicationWifiP2pServerScreen(activity, bwfManager) } + } +} + + +@Composable +fun CommunicationWifiP2pSelectContent(controller: NavController) { + Column { + // client mode + Button(onClick = { controller.navigate("client") }) { Text("Client") } + // server mode + Button(onClick = { controller.navigate("server") }) { Text("Server") } + } +} 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 new file mode 100644 index 0000000..f608bc0 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/communication/wifiP2p/server/screen.kt @@ -0,0 +1,106 @@ +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.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) { + val client = remember { mutableStateOf(null) } + + // if the server is not created, prompt the user for the server configuration + if (client.value == null) CommunicationWifiP2pServerContent(activity, bwfManager, client) + // else, go to the base tasks screen + else TaskSessionScreen(activity, client.value!!) +} + + +@Composable +fun CommunicationWifiP2pServerContent( + activity: Activity, + bwfManager: BwfManager, + 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 = { + // TODO(Faraphel): should be merged with the internet server + + // Reset the database | TODO(Faraphel): only for testing purpose + activity.deleteDatabase("local") + + // Create the database + val database = Room.databaseBuilder( + activity, + TaskDatabase::class.java, + "local" + ).build() + + bwfManager.recreateGroup { + // Create the server + Log.i("room-server", "creating the server") + val server = TaskServer(serverPort.intValue, database) + server.start() + + // Get the client from the server + client.value = server.getClientAdmin() + } + }) { + Text("Create") + } + } +} \ 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 new file mode 100644 index 0000000..b14c204 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/screen/task/screen.kt @@ -0,0 +1,55 @@ +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/widgets/Group.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/Group.kt deleted file mode 100644 index 42c2c51..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/Group.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.faraphel.tasks_valider.ui.widgets - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import com.faraphel.tasks_valider.database.entities.Group - -@Composable -fun WidgetGroup(group: Group) { - // TODO - Column { - Text(text = group.name!!) - - // group.tasks.forEach { task -> - // WidgetTask(task) - // } - } -} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/Task.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/Task.kt deleted file mode 100644 index eddc7fd..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/Task.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.faraphel.tasks_valider.ui.widgets - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import com.faraphel.tasks_valider.database.entities.Task - -@Composable -fun WidgetTask(task: Task) { - // task information - Column { - Text(text = task.title) - Text(text = task.description) - } -} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/TaskGroup.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/TaskGroup.kt deleted file mode 100644 index 0055489..0000000 --- a/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/TaskGroup.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.faraphel.tasks_valider.ui.widgets - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.faraphel.tasks_valider.database.Database -import com.faraphel.tasks_valider.database.entities.TaskGroup - -@Composable -fun WidgetTaskStudent(database: Database, taskStudent: TaskGroup) { - val teacherDao = database.teacherDao() - - Column { - // row for this task - Row { - // task information - // TODO: WidgetTask(task = taskStudent.task) - - // align the other columns to the right - Spacer(modifier = Modifier.weight(1f)) - - // task status - Checkbox( - checked = taskStudent.approvalStatus, - onCheckedChange = { status -> taskStudent.approvalStatus = status } - ) - } - - // if the task has been approved - if (taskStudent.approvalStatus) { - Row ( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center - ) { - // teacher who approved the task - Text(text = teacherDao.getById(taskStudent.approvalTeacherId!!).fullName) - - // align the other columns to the right - Spacer(modifier = Modifier.width(16.dp)) - - // date of approval - Text(text = taskStudent.approvalTime.toString()) - } - } - } -} diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/connectivity/WifiP2pDeviceListWidget.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/connectivity/WifiP2pDeviceListWidget.kt new file mode 100644 index 0000000..9c09b82 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/connectivity/WifiP2pDeviceListWidget.kt @@ -0,0 +1,39 @@ +package com.faraphel.tasks_valider.ui.widgets.connectivity + +import android.net.wifi.p2p.WifiP2pDevice +import android.net.wifi.p2p.WifiP2pDeviceList +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState + + +/** + * Represent a list of WiFi-Direct devices. + * @param peers the list of peers to represent + * @param filter a filter for the peers + * @param deviceState a state containing the selected device + */ +@Composable +fun WifiP2pDeviceListWidget( + peers: WifiP2pDeviceList?, + filter: ((WifiP2pDevice) -> Boolean)? = null, + deviceState: MutableState? = null, +) { + Text(text = "Devices (${peers?.deviceList?.size ?: 0})") + + Column { + // if there are peers to display + if (peers != null) { + // for every device in the list + for (device in peers.deviceList) { + // if the filter (if set) does not apply to this device, ignore it + if (filter != null && !filter(device)) + continue + + // create a new object for the device + WifiP2pDeviceWidget(device, deviceState) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/connectivity/WifiP2pDeviceWidget.kt b/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/connectivity/WifiP2pDeviceWidget.kt new file mode 100644 index 0000000..71e9b70 --- /dev/null +++ b/app/src/main/java/com/faraphel/tasks_valider/ui/widgets/connectivity/WifiP2pDeviceWidget.kt @@ -0,0 +1,44 @@ +package com.faraphel.tasks_valider.ui.widgets.connectivity + +import android.net.wifi.p2p.WifiP2pDevice +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.graphics.Color + + +/** + * A widget that represent a WiFi-Direct device. + * @param device the device that should be represented + * @param deviceState a state that will be updated to the device when it is selected + */ +@Composable +fun WifiP2pDeviceWidget(device: WifiP2pDevice, deviceState: MutableState? = null) { + Button(onClick = { if (deviceState != null) deviceState.value = device }) { + Column { + Row { + Text(text = "Name: ") + Text(text = device.deviceName) + } + Row { + Text(text = "Is Owner: ") + Text(text = device.isGroupOwner.toString()) + } + Row { + Text(text = "Address: ", color = Color.LightGray) + Text(text = device.deviceAddress, color = Color.LightGray) + } + Row { + Text(text = "Primary Type: ", color = Color.Green) + Text(text = device.primaryDeviceType, color = Color.Green) + } + Row { + Text(text = "Status: ", color = Color.Yellow) + Text(text = device.status.toString(), color = Color.Yellow) + } + } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 5cdb21d..b87154e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("com.android.application") version "8.2.2" apply false - id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("com.google.devtools.ksp") version "1.9.21-1.0.15" apply false -} \ No newline at end of file + kotlin("android") version "1.9.23" apply false + kotlin("plugin.serialization") version "1.9.23" apply false +} diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega