diff --git a/README.md b/README.md index 17e3bd2..4b0b165 100644 --- a/README.md +++ b/README.md @@ -1 +1,24 @@ # M1-Projet + +Cette application est la partie cliente du projet Palto. + +## Fonctionnalités + +- Se connecter sur un serveur Palto avec ses identifiants, ou bien se connecter en anonyme. +- Sur le menu : + - Voir la liste des sessions créées. + - Créer une nouvelle session en lui donnant un nom. + - Ouvrir une session en cliquant sur l’item. +- Lorsqu’une session est ouverte : + - Voir la list des présences. + - Scanner une carte NFC et créer un nouvel étudiant associé à cette carte. + - Ajouter manuellement un étudiant à la liste. + +## Prérequis + +- Android 8.1 SDK 27. +- Support du NFC + +## Utilisation + +Importer le projet dans Android Studio. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 39d63cb..e50f1e9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,7 +29,6 @@ android { kotlinOptions { jvmTarget = "1.8" } - buildToolsVersion = "33.0.1" buildFeatures { viewBinding = true } @@ -38,16 +37,21 @@ android { dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.10.0") + implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") - implementation("androidx.navigation:navigation-ui-ktx:2.7.5") - implementation("androidx.annotation:annotation:1.7.0") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") + implementation("androidx.navigation:navigation-ui-ktx:2.7.6") + implementation("androidx.annotation:annotation:1.7.1") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") implementation("androidx.legacy:legacy-support-v4:1.0.0") - implementation("androidx.recyclerview:recyclerview:1.3.0") + implementation("androidx.recyclerview:recyclerview:1.3.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + // Retrofit and Moshi for API requests. + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-moshi:2.9.0") + implementation("com.squareup.moshi:moshi-kotlin:1.13.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fc2cb02..cf82b15 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + >() + val tagId: LiveData> = _tagId + + @OptIn(ExperimentalStdlibApi::class) + fun setTag(tag: Tag) { + Log.d("Nfc", "A new tag has been set.") + _tagId.postValue(Event(tag.id.toHexString())) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/PaltoActivity.kt b/app/src/main/java/com/example/palto/PaltoActivity.kt index 8a70afa..02773bd 100644 --- a/app/src/main/java/com/example/palto/PaltoActivity.kt +++ b/app/src/main/java/com/example/palto/PaltoActivity.kt @@ -1,64 +1,96 @@ package com.example.palto import android.nfc.NfcAdapter -import android.nfc.Tag import android.os.Bundle import android.util.Log -import androidx.appcompat.app.AppCompatActivity +import android.view.Menu +import android.view.MenuInflater import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.setupWithNavController +import com.example.palto.databinding.ActivityPaltoBinding +import com.example.palto.ui.CardViewModel class PaltoActivity : AppCompatActivity() { private var nfcAdapter: NfcAdapter? = null - private val paltoViewModel: PaltoViewModel by viewModels() + private val nfcViewModel: NfcViewModel by viewModels() + + private lateinit var binding: ActivityPaltoBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // get the NFC Adapter - nfcAdapter = NfcAdapter.getDefaultAdapter(this) + binding = ActivityPaltoBinding.inflate(layoutInflater) + setContentView(binding.root) - // check if NFC is supported + // + // Toolbar + // + + // Set the toolbar as the app bar for the activity. + setSupportActionBar(binding.paltoToolbar) + + // Configure the app bar + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.palto_nav_host_fragment) as NavHostFragment + val navController = navHostFragment.navController + val appBarConfiguration = AppBarConfiguration( + setOf( + R.id.menuFragment, R.id.loginFragment + ) + ) + binding.paltoToolbar.setupWithNavController(navController, appBarConfiguration) + + // + // NFC Adapter + // + + nfcAdapter = NfcAdapter.getDefaultAdapter(this) + // Check if NFC is supported (already checked in the app manifest). if (nfcAdapter == null) { Log.e("NFC", "NFC is not supported") - return } - - // check if NFC is disabled if (nfcAdapter?.isEnabled == false) { Log.w("NFC", "NFC is not enabled") } - - setContentView(R.layout.activity_palto) - /* - val url = URL("https://www.faraphel.fr/palto/api/auth/token/") - val connection = url.openConnection() - val auth_data = Json.decodeFromString(connection.content) - */ } + /** + * Specify the options menu for the Activity. + */ + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + val inflater: MenuInflater = menuInflater + inflater.inflate(R.menu.palto_menu, menu) + return true + } + + /** + * Just before the application is displayed. + */ override fun onResume() { super.onResume() + // Begin to read NFC Cards. nfcAdapter?.enableReaderMode( this, - paltoViewModel.tagLiveData::postValue, + nfcViewModel::setTag, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ) } + /** + * Just after the application has been quit. + */ override fun onPause() { super.onPause() - // disable the NFC discovery + // Disable the NFC discovery. nfcAdapter?.disableReaderMode(this) } - - @OptIn(ExperimentalStdlibApi::class) - fun processTag(tag: Tag) { - Log.d("NFC", "Tag ID : " + tag.id.toHexString()) - } } diff --git a/app/src/main/java/com/example/palto/PaltoViewModel.kt b/app/src/main/java/com/example/palto/PaltoViewModel.kt deleted file mode 100644 index 9d69dc4..0000000 --- a/app/src/main/java/com/example/palto/PaltoViewModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.palto - -import android.nfc.Tag -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class PaltoViewModel: ViewModel() { - val tagLiveData = MutableLiveData() -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/Result.kt b/app/src/main/java/com/example/palto/data/Result.kt deleted file mode 100644 index be27f93..0000000 --- a/app/src/main/java/com/example/palto/data/Result.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.palto.data - -/** - * A generic class that holds a value with its loading status. - * @param - */ -sealed class Result { - - data class Success(val data: T) : Result() - data class Error(val exception: Exception) : Result() - - override fun toString(): String { - return when (this) { - is Success<*> -> "Success[data=$data]" - is Error -> "Error[exception=$exception]" - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/local/LocalDataSource.kt b/app/src/main/java/com/example/palto/data/local/LocalDataSource.kt deleted file mode 100644 index d05487e..0000000 --- a/app/src/main/java/com/example/palto/data/local/LocalDataSource.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.palto.data.local - -/** - * Class that handles authentication w/ login credentials and retrieves user information. - */ -class LocalDataSource { - /* - fun login(username: String, password: String): Result { - try { - - - val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe") - return Result.Success(fakeUser) - - } catch (e: Throwable) { - return Result.Error(IOException("Error logging in", e)) - } - } - - fun logout() { - // TODO: revoke authentication - } - */ -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/network/PaltoApiService.kt b/app/src/main/java/com/example/palto/data/network/PaltoApiService.kt new file mode 100644 index 0000000..21cde23 --- /dev/null +++ b/app/src/main/java/com/example/palto/data/network/PaltoApiService.kt @@ -0,0 +1,46 @@ +package com.example.palto.data.network + +import com.example.palto.data.network.model.UserCredentials +import com.example.palto.domain.Tokens +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.http.Body +import retrofit2.http.POST + +/** + * A public Api object that exposes the lazy-initialized Retrofit service + */ +object PaltoApi { + + // Build the Moshi object that Retrofit will be using, making sure to add the Kotlin adapter for + // full Kotlin compatibility. + private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + fun createService(url: String) { + // Use the Retrofit builder to build a retrofit object using a Moshi converter + // with our Moshi object. + val retrofit = Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .baseUrl(url) + .build() + retrofitService = retrofit.create(PaltoApiService::class.java) + } + + // Retrofit service that Palto will use to do requests. + // Make sure to call createService once before using it. + lateinit var retrofitService: PaltoApiService +} + + +/** + * Functions to query the API. + */ +interface PaltoApiService { + + @POST("api/auth/jwt/token/") + suspend fun getTokens(@Body userCredentials: UserCredentials): Tokens +} diff --git a/app/src/main/java/com/example/palto/data/network/ServerDataSource.kt b/app/src/main/java/com/example/palto/data/network/ServerDataSource.kt deleted file mode 100644 index 8c68892..0000000 --- a/app/src/main/java/com/example/palto/data/network/ServerDataSource.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.palto.data.network - -import com.example.palto.data.Result -import com.example.palto.model.LoggedInUser -import com.example.palto.model.Tokens -import java.io.IOException -import java.util.UUID - -/** - * Class that handles API calls. - */ -class ServerDataSource { - - private var hostname: String? = null - - fun requestToken( - hostname: String, - username: String, - password: String - ): Result { - try { - val tokens = Tokens( - refresh = "aa", - access = "bb" - ) - return Result.Success(tokens) - } catch (e: Throwable) { - return Result.Error(IOException("Error logging in", e)) - } - } - - fun refreshToken(current_tokens: Tokens): Result { - return Result.Success(current_tokens) - } - - fun verifyToken(): Boolean { - return true - } - - fun login( - hostname: String, - username: String, - password: String - ): Result { - try { - val fakeUser = LoggedInUser( - UUID.randomUUID().toString(), - "dede", - "Lucie", - "Doe", - "aa@free.fr", - ) - return Result.Success(fakeUser) - } catch (e: Throwable) { - return Result.Error(IOException("Error logging in", e)) - } - } - - fun logout() { } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/network/model/UserCredentials.kt b/app/src/main/java/com/example/palto/data/network/model/UserCredentials.kt new file mode 100644 index 0000000..50b3ffd --- /dev/null +++ b/app/src/main/java/com/example/palto/data/network/model/UserCredentials.kt @@ -0,0 +1,6 @@ +package com.example.palto.data.network.model + +data class UserCredentials( + val username: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/repository/AttendanceRepository.kt b/app/src/main/java/com/example/palto/data/repository/AttendanceRepository.kt index 13a8631..5ceb649 100644 --- a/app/src/main/java/com/example/palto/data/repository/AttendanceRepository.kt +++ b/app/src/main/java/com/example/palto/data/repository/AttendanceRepository.kt @@ -1,10 +1,3 @@ package com.example.palto.data.repository -import com.example.palto.data.network.ServerDataSource - -/** - * - */ -class AttendanceRepository(val dataSource: ServerDataSource) { - // private val cards -} \ No newline at end of file +class AttendanceRepository \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/repository/LoginRepository.kt b/app/src/main/java/com/example/palto/data/repository/LoginRepository.kt deleted file mode 100644 index ad61888..0000000 --- a/app/src/main/java/com/example/palto/data/repository/LoginRepository.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.example.palto.data.repository - -import com.example.palto.data.Result -import com.example.palto.data.network.ServerDataSource -import com.example.palto.model.LoggedInUser - -/** - * Class that requests authentication and user information from the remote data source and - * maintains an in-memory cache of login status and user credentials information. - */ - -class LoginRepository(val dataSource: ServerDataSource) { - - var user: LoggedInUser? = null - private set - - val isLoggedIn: Boolean - get() = user != null - - init { - user = null - } - - fun logout() { - user = null - dataSource.logout() - } - - fun login( - hostname: String, - username: String, - password: String - ): Result { - // handle login - val result = dataSource.login(hostname, username, password) - - if (result is Result.Success) { - setLoggedInUser(result.data) - } - - return result - } - - private fun setLoggedInUser(loggedInUser: LoggedInUser) { - this.user = loggedInUser - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/repository/SessionRepository.kt b/app/src/main/java/com/example/palto/data/repository/SessionRepository.kt index 3b4b4a7..45e122c 100644 --- a/app/src/main/java/com/example/palto/data/repository/SessionRepository.kt +++ b/app/src/main/java/com/example/palto/data/repository/SessionRepository.kt @@ -1,10 +1,3 @@ package com.example.palto.data.repository -import com.example.palto.data.network.ServerDataSource - -/** - * - */ -class SessionRepository(val dataSource: ServerDataSource) { - // private val cards -} \ No newline at end of file +class SessionRepository \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/repository/TokenRepository.kt b/app/src/main/java/com/example/palto/data/repository/TokenRepository.kt new file mode 100644 index 0000000..c1bb243 --- /dev/null +++ b/app/src/main/java/com/example/palto/data/repository/TokenRepository.kt @@ -0,0 +1,23 @@ +package com.example.palto.data.repository + +import com.example.palto.data.network.PaltoApi +import com.example.palto.data.network.model.UserCredentials +import com.example.palto.domain.Tokens + +/** + * Class that requests authentication tokens from Palto server. + */ +class TokenRepository() { + + private var tokens: Tokens? = null + + suspend fun authenticate( + hostname: String, + username: String, + password: String + ) { + PaltoApi.createService("http://$hostname:8000/") + val tokens = PaltoApi.retrofitService.getTokens((UserCredentials(username, password))) + this.tokens = tokens + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/repository/TokensRepository.kt b/app/src/main/java/com/example/palto/data/repository/TokensRepository.kt deleted file mode 100644 index 184f04c..0000000 --- a/app/src/main/java/com/example/palto/data/repository/TokensRepository.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.palto.data.repository - -import com.example.palto.data.network.ServerDataSource -import com.example.palto.model.Tokens - -/** - * Class that requests authentication and user information from the remote data source and - * maintains an in-memory cache of login status and user credentials information. - */ - -class TokensRepository(val dataSource: ServerDataSource) { - - var tokens: Tokens? = null - private set - - /* - val isLoggedIn: Boolean - get() = user != null - */ - - init { - // If user credentials will be cached in local storage, it is recommended it be encrypted - // @see https://developer.android.com/training/articles/keystore - tokens = null - } - - /* - fun login(username: String, password: String): Result { - // handle login - val result = dataSource.login(username, password) - - if (result is Result.Success) { - setLoggedInUser(result.data) - } - - return result - } - */ - - private fun setTokens(tokens: Tokens) { - this.tokens = tokens - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/data/repository/UserRepository.kt b/app/src/main/java/com/example/palto/data/repository/UserRepository.kt new file mode 100644 index 0000000..2c58c10 --- /dev/null +++ b/app/src/main/java/com/example/palto/data/repository/UserRepository.kt @@ -0,0 +1,3 @@ +package com.example.palto.data.repository + +class UserRepository \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/model/Attendance.kt b/app/src/main/java/com/example/palto/domain/Attendance.kt similarity index 56% rename from app/src/main/java/com/example/palto/model/Attendance.kt rename to app/src/main/java/com/example/palto/domain/Attendance.kt index 1288725..1d6c321 100644 --- a/app/src/main/java/com/example/palto/model/Attendance.kt +++ b/app/src/main/java/com/example/palto/domain/Attendance.kt @@ -1,10 +1,12 @@ -package com.example.palto.model +package com.example.palto.domain import java.io.Serializable +import java.time.LocalTime /** * Data class that captures tokens for logged in users retrieved from LoginRepository */ data class Attendance( - val date: String, - val access: String + val id: Int, + val student: User, + val date: LocalTime ) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/domain/Card.kt b/app/src/main/java/com/example/palto/domain/Card.kt new file mode 100644 index 0000000..0520b42 --- /dev/null +++ b/app/src/main/java/com/example/palto/domain/Card.kt @@ -0,0 +1,7 @@ +package com.example.palto.domain + +data class Card( + val id: Int, + val tagId: String, + val user: User, +) \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/domain/Event.kt b/app/src/main/java/com/example/palto/domain/Event.kt new file mode 100644 index 0000000..0b72683 --- /dev/null +++ b/app/src/main/java/com/example/palto/domain/Event.kt @@ -0,0 +1,18 @@ +package com.example.palto.domain + +class Event(private val content: T) { + + var hasBeenHandled = false + private set + + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + fun peekContent(): T = content +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/domain/Session.kt b/app/src/main/java/com/example/palto/domain/Session.kt new file mode 100644 index 0000000..4e2116e --- /dev/null +++ b/app/src/main/java/com/example/palto/domain/Session.kt @@ -0,0 +1,12 @@ +package com.example.palto.domain +import java.io.Serializable + +/** + * Data class that captures tokens for logged in users retrieved from LoginRepository + */ +data class Session( + val id: Int, + val name: String, + var attendances: List + // When the list is updated, it is replaced by a new one. +) \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/model/Tokens.kt b/app/src/main/java/com/example/palto/domain/Tokens.kt similarity index 79% rename from app/src/main/java/com/example/palto/model/Tokens.kt rename to app/src/main/java/com/example/palto/domain/Tokens.kt index fa90475..4e29846 100644 --- a/app/src/main/java/com/example/palto/model/Tokens.kt +++ b/app/src/main/java/com/example/palto/domain/Tokens.kt @@ -1,4 +1,4 @@ -package com.example.palto.model +package com.example.palto.domain import java.io.Serializable /** @@ -7,4 +7,4 @@ import java.io.Serializable data class Tokens( val refresh: String, val access: String -) : Serializable \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/domain/User.kt b/app/src/main/java/com/example/palto/domain/User.kt new file mode 100644 index 0000000..7e36833 --- /dev/null +++ b/app/src/main/java/com/example/palto/domain/User.kt @@ -0,0 +1,12 @@ +package com.example.palto.domain + +/** + * Data class that captures user information for logged in users retrieved from LoginRepository + */ +data class User( + val id: Int, + val username: String, + val firstName: String, + val lastName: String, + val email: String +) diff --git a/app/src/main/java/com/example/palto/model/Card.kt b/app/src/main/java/com/example/palto/model/Card.kt deleted file mode 100644 index 8c55422..0000000 --- a/app/src/main/java/com/example/palto/model/Card.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.palto.model -import java.io.Serializable - -data class Card( - val id: String, - val uid: ByteArray, - val department: String, - val owner: String -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Card - - return uid.contentEquals(other.uid) - } - - override fun hashCode(): Int { - return uid.contentHashCode() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/model/LoggedInUser.kt b/app/src/main/java/com/example/palto/model/LoggedInUser.kt deleted file mode 100644 index 6f60811..0000000 --- a/app/src/main/java/com/example/palto/model/LoggedInUser.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.palto.model - -import java.io.Serializable - -/** - * Data class that captures user information for logged in users retrieved from LoginRepository - */ -data class LoggedInUser( - val id: String, - val username: String, - val first_name: String, - val last_name: String, - val email: String -) : Serializable diff --git a/app/src/main/java/com/example/palto/model/Session.kt b/app/src/main/java/com/example/palto/model/Session.kt deleted file mode 100644 index 8a6827f..0000000 --- a/app/src/main/java/com/example/palto/model/Session.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.palto.model -import java.io.Serializable - -/** - * Data class that captures tokens for logged in users retrieved from LoginRepository - */ -data class Session( - val id: String -) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/CardViewModel.kt b/app/src/main/java/com/example/palto/ui/CardViewModel.kt new file mode 100644 index 0000000..7223020 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/CardViewModel.kt @@ -0,0 +1,34 @@ +package com.example.palto.ui + +import android.nfc.Tag +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.palto.domain.Card +import com.example.palto.domain.User + +/** + * CardViewModel maintain a list of cards application wide. + * May be converted in a repository. + */ +class CardViewModel: ViewModel() { + + private var _cards = MutableLiveData>() + private val cards: LiveData> = _cards + + fun createCard(user: User, tagId: String): Card { + val list = _cards.value ?: emptyList() + val card = Card(list.size, tagId, user) + _cards.value = list + card + Log.d("Palto", "CardViewModel: a card has been added into the list.") + return card + } + + fun getCard(tagId: String): Card? { + val card = _cards.value?.find() { + it.tagId == tagId + } + return card + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/UserViewModel.kt b/app/src/main/java/com/example/palto/ui/UserViewModel.kt new file mode 100644 index 0000000..ac791c4 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/UserViewModel.kt @@ -0,0 +1,31 @@ +package com.example.palto.ui + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.palto.domain.Card +import com.example.palto.domain.User + +/** + * UserViewModel maintain a list of users application wide. + * May be converted into a repository. + */ +class UserViewModel: ViewModel() { + + private var _users = MutableLiveData>() + val users : LiveData> = _users + + fun createUser(username: String): User { + val list = _users.value ?: emptyList() + val user = User( + id = list.size, + username = username, + firstName = "", + lastName = "", + email = "") + _users.value = list + user + Log.d("Palto", "UserViewModel: a user has been added into the list.") + return user + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListAdapter.kt b/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListAdapter.kt deleted file mode 100644 index a7788b1..0000000 --- a/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListAdapter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.palto.ui.attendanceList - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.example.palto.databinding.FragmentAttendanceItemBinding -import com.example.palto.model.Card - -/** - * - */ -class AttendanceListAdapter : - ListAdapter(CardDiffCallback) { - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): ViewHolder { - - return ViewHolder( - FragmentAttendanceItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - @OptIn(ExperimentalStdlibApi::class) - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item = getItem(position) - holder.cardId.text = item.uid.toHexString() - //holder.contentView.text = item.content - } - - inner class ViewHolder( - binding: FragmentAttendanceItemBinding - ) : RecyclerView.ViewHolder(binding.root) { - - val cardId: TextView = binding.cardId - override fun toString(): String { - return super.toString() + " '" + cardId.text + "'" - } - } -} - -object CardDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Card, newItem: Card): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: Card, newItem: Card): Boolean { - return oldItem.id == newItem.id - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListFragment.kt b/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListFragment.kt deleted file mode 100644 index 68773ab..0000000 --- a/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListFragment.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.palto.ui.attendanceList - -import android.nfc.NfcAdapter -import android.nfc.Tag -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import androidx.navigation.navGraphViewModels -import com.example.palto.PaltoViewModel -import com.example.palto.R -import com.example.palto.databinding.FragmentAttendanceListBinding - -/** - * A fragment representing a list of attendances. - */ -class AttendanceListFragment : Fragment() { - - private val attendanceListViewModel: AttendanceListViewModel by - navGraphViewModels(R.id.nav_graph) { AttendanceListViewModel.Factory } - - private val paltoViewModel: PaltoViewModel by - activityViewModels() - - private var _binding: FragmentAttendanceListBinding? = null - // This property is only valid between onCreateView and onDestroyView - private val binding get() = _binding!! - - /** - * Only inflate the view. - */ - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - - _binding = FragmentAttendanceListBinding.inflate(inflater, container, false) - return binding.root - } - - /** - * Logic on the returned view of onCreateView. - */ - override fun onViewCreated( - view: View, - savedInstanceState: Bundle? - ) { - super.onViewCreated(view, savedInstanceState) - - // Set the adapter of the view for managing automatically the list of items on the screen. - val adapter = AttendanceListAdapter() - binding.list.adapter = adapter - attendanceListViewModel.cardsLiveData.observe(viewLifecycleOwner) { - Log.d("NFC", "A card has been had to the list") - adapter.submitList(it) - } - - // Set the listener for a new NFC tag. - paltoViewModel.tagLiveData.observe(viewLifecycleOwner) { - Log.d("NFC", "tag observer has been notified") - attendanceListViewModel.insertCard(it) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListViewModel.kt b/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListViewModel.kt deleted file mode 100644 index a4efa38..0000000 --- a/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListViewModel.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.palto.ui.attendanceList - -import android.nfc.Tag -import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.example.palto.data.network.ServerDataSource -import com.example.palto.data.repository.AttendanceRepository -import com.example.palto.model.Card - -/** - * ViewModel of a session which has a list of attendances. - */ -class AttendanceListViewModel( - private val attendanceRepository: AttendanceRepository -) : ViewModel() { - - val cardsLiveData: MutableLiveData> = MutableLiveData(emptyList()) - - fun insertCard(tag: Tag) { - val card = Card( - "0", - tag.id, - "tmp", - "tmp" - ) - cardsLiveData.value = (cardsLiveData.value ?: emptyList()) + card - Log.d("NFC", "view model: A card has been had to the list") - } - - /** - * ViewModel Factory. - */ - companion object { - - val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create( - modelClass: Class - ): T { - return AttendanceListViewModel( - AttendanceRepository(ServerDataSource()) - ) as T - } - } - } -} diff --git a/app/src/main/java/com/example/palto/ui/login/LoggedInUserView.kt b/app/src/main/java/com/example/palto/ui/login/LoggedInUserView.kt deleted file mode 100644 index 8cddb61..0000000 --- a/app/src/main/java/com/example/palto/ui/login/LoggedInUserView.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.palto.ui.login - -/* Est-ce que c’est util ? - * Updater la vue dans le fragment - */ - -/** - * User details post authentication that is exposed to the UI - */ -data class LoggedInUserView( - val displayName: String - //... other data fields that may be accessible to the UI -) diff --git a/app/src/main/java/com/example/palto/ui/login/LoginFragment.kt b/app/src/main/java/com/example/palto/ui/login/LoginFragment.kt index acf12be..92d524c 100644 --- a/app/src/main/java/com/example/palto/ui/login/LoginFragment.kt +++ b/app/src/main/java/com/example/palto/ui/login/LoginFragment.kt @@ -1,42 +1,62 @@ package com.example.palto.ui.login -import androidx.lifecycle.Observer -import androidx.annotation.StringRes -import androidx.fragment.app.Fragment +import android.annotation.SuppressLint import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.EditorInfo import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController -import androidx.navigation.navGraphViewModels import com.example.palto.databinding.FragmentLoginBinding -import com.example.palto.R - class LoginFragment : Fragment() { - private val loginViewModel: LoginViewModel by - navGraphViewModels(R.id.nav_graph) { LoginViewModelFactory() } + // userViewModel is where the user is logged in, at the activity level. + private val loginViewModel: LoginViewModel by activityViewModels() { LoginViewModel.Factory } - private var _binding: FragmentLoginBinding? = null - - // This property is only valid between onCreateView and onDestroyView. - private val binding get() = _binding!! + private lateinit var binding: FragmentLoginBinding override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + binding = FragmentLoginBinding.inflate(inflater, container, false) + + val navController = findNavController() + + // Bind the login button. + binding.login.setOnClickListener { + binding.loading.visibility = View.VISIBLE + loginViewModel.login( + binding.hostname.text.toString(), + binding.username.text.toString(), + binding.password.text.toString() + ) + } + + // Bind anonymous login clickable text. + binding.loginAnonymous.setOnClickListener { + loginViewModel.loginAnonymous() + } + + // On result of logging. + loginViewModel.result.observe(viewLifecycleOwner) { + binding.loading.visibility = View.GONE + if (it.success) { + navController.popBackStack() + } else if (it.error != null) { + binding.loginError.text = "Exception : ${it.exception.toString()}" + Toast.makeText(activity, it.error, Toast.LENGTH_LONG).show() + } + } - _binding = FragmentLoginBinding.inflate(inflater, container, false) return binding.root } + /* override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -46,23 +66,21 @@ class LoginFragment : Fragment() { val loginButton = binding.login val loadingProgressBar = binding.loading - // - loginViewModel.loginFormState.observe(viewLifecycleOwner, - Observer { loginFormState -> - if (loginFormState == null) { - return@Observer - } - loginButton.isEnabled = loginFormState.isDataValid - loginFormState.hostnameError?.let { - hostnameEditText.error = getString(it) - } - loginFormState.usernameError?.let { - usernameEditText.error = getString(it) - } - loginFormState.passwordError?.let { - passwordEditText.error = getString(it) - } - }) + loginViewModel.loginFormState.observe(viewLifecycleOwner) { + if (it == null) { + return@Observer + } + loginButton.isEnabled = it.isDataValid + it.hostnameError?.let { + hostnameEditText.error = getString(it) + } + it.usernameError?.let { + usernameEditText.error = getString(it) + } + it.passwordError?.let { + passwordEditText.error = getString(it) + } + } loginViewModel.loginResult.observe(viewLifecycleOwner, Observer { loginResult -> @@ -103,17 +121,8 @@ class LoginFragment : Fragment() { } false } - - // Damien : Le setOnClickListener est là ! - loginButton.setOnClickListener { - loadingProgressBar.visibility = View.VISIBLE - loginViewModel.login( - hostnameEditText.text.toString(), - usernameEditText.text.toString(), - passwordEditText.text.toString() - ) - } } + */ /* private fun updateUiWithUser(model: LoggedInUserView) { @@ -122,15 +131,11 @@ class LoginFragment : Fragment() { val appContext = context?.applicationContext ?: return Toast.makeText(appContext, welcome, Toast.LENGTH_LONG).show() } - */ private fun showLoginFailed(@StringRes errorString: Int) { val appContext = context?.applicationContext ?: return Toast.makeText(appContext, errorString, Toast.LENGTH_LONG).show() } - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } + */ } \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/login/LoginResult.kt b/app/src/main/java/com/example/palto/ui/login/LoginResult.kt index 9367f89..1d748f2 100644 --- a/app/src/main/java/com/example/palto/ui/login/LoginResult.kt +++ b/app/src/main/java/com/example/palto/ui/login/LoginResult.kt @@ -1,9 +1,10 @@ package com.example.palto.ui.login /** - * Authentication result : success (user details) or error message. + * Authentication result : success is true if connected or error message with exception. */ data class LoginResult( - val success: LoggedInUserView? = null, - val error: Int? = null + val success: Boolean, + val error: Int? = null, // Id of the string resource to display to the user. + val exception: Exception? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/login/LoginViewModel.kt b/app/src/main/java/com/example/palto/ui/login/LoginViewModel.kt index 99ea964..f7365dc 100644 --- a/app/src/main/java/com/example/palto/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/example/palto/ui/login/LoginViewModel.kt @@ -1,84 +1,106 @@ package com.example.palto.ui.login +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import android.util.Patterns import androidx.lifecycle.ViewModelProvider -import com.example.palto.data.repository.LoginRepository - +import androidx.lifecycle.viewModelScope import com.example.palto.R -import com.example.palto.data.network.ServerDataSource +import com.example.palto.data.repository.TokenRepository +import com.example.palto.data.repository.UserRepository +import com.example.palto.domain.User +import kotlinx.coroutines.launch -class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { +/** + * LoginViewModel to access information about the logged in user and login form. + */ +class LoginViewModel( + private val tokenRepository: TokenRepository, + private val userRepository: UserRepository +): ViewModel() { - private val _loginForm = MutableLiveData() - val loginFormState: LiveData = _loginForm + private var _result = MutableLiveData() + val result = _result as LiveData - private val _loginResult = MutableLiveData() - val loginResult: LiveData = _loginResult + // User is initially set to null to be disconnected. + private var _user = MutableLiveData(null) + val user = _user as LiveData + + /* + private val _loginFormState = MutableLiveData() + val loginFormState: LiveData = _loginFormState + */ fun login( hostname: String, username: String, password: String) { - // can be launched in a separate asynchronous job - val result = loginRepository.login(hostname, username, password) - /* - if (result is Result.Success) { - _loginResult.value = - LoginResult(success = LoggedInUserView( - displayName = result.data.displayName)) - } else { - _loginResult.value = LoginResult(error = R.string.login_failed) - } - */ - } - - fun loginDataChanged( - hostname: String, - username: String, - password: String) { - if (!isHostNameValid(hostname)) { - _loginForm.value = LoginFormState(hostnameError = R.string.invalid_hostname) - } else if (!isUserNameValid(username)) { - _loginForm.value = LoginFormState(usernameError = R.string.invalid_username) - } else if (!isPasswordValid(password)) { - _loginForm.value = LoginFormState(passwordError = R.string.invalid_password) - } else { - _loginForm.value = LoginFormState(isDataValid = true) - } - } - - private fun isHostNameValid(hostname: String): Boolean { - return hostname.isNotBlank() - } - - // A placeholder username validation check - private fun isUserNameValid(username: String): Boolean { - return if (username.contains("@")) { - Patterns.EMAIL_ADDRESS.matcher(username).matches() - } else { - username.isNotBlank() - } - } - - // A placeholder password validation check - private fun isPasswordValid(password: String): Boolean { - return password.length > 5 - } -} - -class LoginViewModelFactory : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { - return LoginViewModel( - loginRepository = LoginRepository( - dataSource = ServerDataSource() + // Coroutine runs in background. + viewModelScope.launch { + try { + tokenRepository.authenticate(hostname, username, password) + _user.value = User(-1, username, "", "", "") + _result.value = LoginResult(success = true) + } catch (e: Exception) { + Log.e("Palto", "Connection error: " + e.message) + _result.value = LoginResult( + success = false, + error = R.string.login_failed, + exception = e ) - ) as T + } } - throw IllegalArgumentException("Unknown ViewModel class") } -} + + fun loginAnonymous() { + _user.value = User(-2, "anonymous", "", "", "") + _result.value = LoginResult(success = true) + } + + fun logout() { + _user.value = null + _result.value = LoginResult(success = false) + } + + /* + fun loginDataChanged( + hostname: String, + username: String, + password: String) { + if (!isHostNameValid(hostname)) { + _loginForm.value = LoginFormState(hostnameError = R.string.invalid_hostname) + } else if (!isUserNameValid(username)) { + _loginForm.value = LoginFormState(usernameError = R.string.invalid_username) + } else if (!isPasswordValid(password)) { + _loginForm.value = LoginFormState(passwordError = R.string.invalid_password) + } else { + _loginForm.value = LoginFormState(isDataValid = true) + } + } + private fun isHostNameValid(hostname: String): Boolean { + return hostname.isNotBlank() + } + + private fun isUserNameValid(username: String): Boolean { + return username.isNotBlank() + } + + private fun isPasswordValid(password: String): Boolean { + return password.length > 5 + } + */ + + companion object { + + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return LoginViewModel( + tokenRepository = TokenRepository(), + userRepository = UserRepository() + ) as T + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/menu/MenuAdapter.kt b/app/src/main/java/com/example/palto/ui/menu/MenuAdapter.kt new file mode 100644 index 0000000..518c04f --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/menu/MenuAdapter.kt @@ -0,0 +1,58 @@ +package com.example.palto.ui.menu + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.palto.databinding.FragmentMenuItemBinding +import com.example.palto.domain.Session + +/** + * A [ListAdapter] that can display [Session] items. + */ +class MenuAdapter(private val onClick: (Session) -> Unit) : + ListAdapter(SessionDiffCallback) { + inner class ViewHolder(binding: FragmentMenuItemBinding) : + RecyclerView.ViewHolder(binding.root) { + private val sessionNameText: TextView = binding.sessionName + private var currentSession: Session? = null + + init { + binding.root.setOnClickListener { + currentSession?.let { + onClick(it) + } + } + } + + fun bind(session: Session) { + currentSession = session + sessionNameText.text = session.name + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = FragmentMenuItemBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item) + } + + override fun getItemCount() = currentList.size +} + +object SessionDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Session, newItem: Session): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: Session, newItem: Session): Boolean { + return oldItem.id == newItem.id + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/menu/MenuFragment.kt b/app/src/main/java/com/example/palto/ui/menu/MenuFragment.kt new file mode 100644 index 0000000..2991f91 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/menu/MenuFragment.kt @@ -0,0 +1,71 @@ +package com.example.palto.ui.menu + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import com.example.palto.R +import com.example.palto.databinding.FragmentMenuListBinding +import com.example.palto.domain.Session +import com.example.palto.ui.login.LoginViewModel + +/** + * A fragment representing a list of Sessions. + */ +class MenuFragment : Fragment() { + + private val menuViewModel: MenuViewModel by + navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory } + + private val loginViewModel: LoginViewModel by + activityViewModels() { LoginViewModel.Factory } + + // This property is only valid between onCreateView and onDestroyView + private lateinit var binding: FragmentMenuListBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentMenuListBinding.inflate(inflater, container, false) + + val navController = findNavController() + + // Connect the user. + loginViewModel.user.observe(viewLifecycleOwner) { + if (it != null) { + // Get sessions of the user from remote. + } else { + navController.navigate(R.id.loginFragment) + } + } + + // Display the list of sessions. + + // Create a new MenuAdapter (list) with the given function when clicking an item. + val adapter = MenuAdapter { adapterOnClick(it) } + binding.menuList.adapter = adapter + // Link the adapter with the session list in the menuViewMode. + menuViewModel.sessions.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + + // Bind the add button. + binding.createSession.setOnClickListener { + navController.navigate(R.id.action_menuFragment_to_newSessionFragment) + } + + return binding.root + } + + private fun adapterOnClick(session: Session) { + val bundle = bundleOf("session" to session.id) + findNavController().navigate(R.id.action_menuFragment_to_sessionFragment, bundle) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/menu/MenuViewModel.kt b/app/src/main/java/com/example/palto/ui/menu/MenuViewModel.kt new file mode 100644 index 0000000..e263c6d --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/menu/MenuViewModel.kt @@ -0,0 +1,47 @@ +package com.example.palto.ui.menu + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.palto.domain.Attendance +import com.example.palto.domain.Session + +/** + * ViewModel for accessing all the sessions created. + */ +class MenuViewModel() : ViewModel() { + + // A list of sessions. + private var _sessions = MutableLiveData>() + val sessions: LiveData> = _sessions + + fun createSession(name: String) { + val list = _sessions.value ?: emptyList() + val session = Session( + id = list.size, + name = name, + attendances = emptyList() + ) + _sessions.value = list + session + Log.d("Palto", "MenuViewModel: A session has been added into the list.") + } + + fun getSession(id: Int): Session? { + return _sessions.value?.find { it.id == id } + } + + fun setAttendanceListSession(id: Int, list: List) { + getSession(id)?.let { it.attendances = list } + } + + companion object { + + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return MenuViewModel() as T + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/menu/new_session/NewSessionFragment.kt b/app/src/main/java/com/example/palto/ui/menu/new_session/NewSessionFragment.kt new file mode 100644 index 0000000..fdbe854 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/menu/new_session/NewSessionFragment.kt @@ -0,0 +1,46 @@ +package com.example.palto.ui.menu.new_session + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.view.menu.MenuView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import com.example.palto.R +import com.example.palto.databinding.FragmentNewSessionBinding +import com.example.palto.ui.menu.MenuViewModel + +/** + * + */ +class NewSessionFragment : Fragment() { + + private val newSessionViewModel: NewSessionViewModel by viewModels() + + private val menuViewModel: MenuViewModel by + navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory } + + // This property is only valid between onCreateView and onDestroyView + private lateinit var binding: FragmentNewSessionBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentNewSessionBinding.inflate(inflater, container, false) + + // Bind the create button + binding.newSessionCreate.setOnClickListener { + menuViewModel.createSession( + name = binding.newSessionName.text.toString() + ) + findNavController().popBackStack() + } + + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/menu/new_session/NewSessionViewModel.kt b/app/src/main/java/com/example/palto/ui/menu/new_session/NewSessionViewModel.kt new file mode 100644 index 0000000..0058596 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/menu/new_session/NewSessionViewModel.kt @@ -0,0 +1,8 @@ +package com.example.palto.ui.menu.new_session + +import androidx.lifecycle.ViewModel + +/** + * ViewModel of the session creation form. Used for verification. + */ +class NewSessionViewModel() : ViewModel() diff --git a/app/src/main/java/com/example/palto/ui/session/SessionAdapter.kt b/app/src/main/java/com/example/palto/ui/session/SessionAdapter.kt new file mode 100644 index 0000000..afb2fd0 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/session/SessionAdapter.kt @@ -0,0 +1,52 @@ +package com.example.palto.ui.session + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.palto.databinding.FragmentSessionItemBinding +import com.example.palto.domain.Attendance +import com.example.palto.domain.Card +import java.time.format.DateTimeFormatter + +/** + * A [ListAdapter] that can display [Attendance] items. + */ +class SessionAdapter : ListAdapter(AttendanceDiffCallback) { + + + inner class ViewHolder(binding: FragmentSessionItemBinding) : + RecyclerView.ViewHolder(binding.root) { + private val attendanceUsernameText: TextView = binding.attendanceUsername + private val attendanceDate: TextView = binding.attendanceDate + + fun bind(attendance: Attendance) { + attendanceUsernameText.text = attendance.student.username + attendanceDate.text = attendance.date.format(DateTimeFormatter.ofPattern("HH:mm:ss")) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = FragmentSessionItemBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + @OptIn(ExperimentalStdlibApi::class) + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item) + } +} + +object AttendanceDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Attendance, newItem: Attendance): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: Attendance, newItem: Attendance): Boolean { + return oldItem.id == newItem.id + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/session/SessionFragment.kt b/app/src/main/java/com/example/palto/ui/session/SessionFragment.kt new file mode 100644 index 0000000..35e7e82 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/session/SessionFragment.kt @@ -0,0 +1,106 @@ +package com.example.palto.ui.session + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import com.example.palto.NfcViewModel +import com.example.palto.R +import com.example.palto.databinding.FragmentSessionListBinding +import com.example.palto.ui.CardViewModel +import com.example.palto.ui.menu.MenuViewModel + +/** + * A fragment representing a list of attendances. + */ +class SessionFragment : Fragment() { + + private val sessionViewModel: SessionViewModel by + navGraphViewModels(R.id.nav_graph) { SessionViewModel.Factory } + + private val menuViewModel: MenuViewModel by + navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory } + + private val cardViewModel: CardViewModel by + navGraphViewModels(R.id.nav_graph) + + private val nfcViewModel: NfcViewModel by activityViewModels() + + // This property is only valid between onCreateView and onDestroyView + private lateinit var binding: FragmentSessionListBinding + + /** + * Have the fragment instantiate the user interface. + */ + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentSessionListBinding.inflate(inflater, container, false) + + // If the bundle value id of key "session" is not empty, + // set the attendance list to that of the selected session. + arguments?.getInt("session")?.let { id -> + val session = menuViewModel.getSession(id) + if (session != null) { + Log.d("Palto", "SessionFragment: Session id ${session.id} has been found.") + sessionViewModel.session = session + sessionViewModel.setAttendanceList(session.attendances) + } + } + + // Set the adapter of the view for managing automatically the list of items on the screen. + val adapter = SessionAdapter() + binding.sessionList.adapter = adapter + sessionViewModel.attendances.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + + // Set the listener for a new NFC tag. + nfcViewModel.tagId.observe(viewLifecycleOwner) { + + // If the NFC tag has not been handled. + it.getContentIfNotHandled()?.let { cardId -> + val card = cardViewModel.getCard(cardId) + // If a card with this tag exists, add this card. + if (card != null) { + sessionViewModel.addAttendance(card.user) + // Else go to the NewStudentFragment to create a new card and student. + } else { + val bundle = bundleOf("tagId" to cardId) + findNavController() + .navigate(R.id.action_sessionFragment_to_newStudentFragment, bundle) + } + } + } + + // Manual add student button + binding.addStudent.setOnClickListener { + findNavController().navigate(R.id.action_sessionFragment_to_newStudentFragment) + } + + // Print the result of adding an attendance on the view. + sessionViewModel.result.observe(viewLifecycleOwner) { + // If the result has not been already shown + it.getContentIfNotHandled()?.let { result -> + Toast.makeText( + activity, + getString(result.message, result.username), + Toast.LENGTH_LONG).show() + } + } + + return binding.root + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/session/SessionResult.kt b/app/src/main/java/com/example/palto/ui/session/SessionResult.kt new file mode 100644 index 0000000..60b0b31 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/session/SessionResult.kt @@ -0,0 +1,10 @@ +package com.example.palto.ui.session + +/** + * Authentication result : success is true if connected or error message with exception. + */ +data class SessionResult( + val success: Boolean, + val message: Int, // Id of the string resource to display to the user + val username: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/session/SessionViewModel.kt b/app/src/main/java/com/example/palto/ui/session/SessionViewModel.kt new file mode 100644 index 0000000..eb81aa5 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/session/SessionViewModel.kt @@ -0,0 +1,84 @@ +package com.example.palto.ui.session + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.palto.R +import com.example.palto.domain.Attendance +import com.example.palto.domain.Event +import com.example.palto.domain.Session +import com.example.palto.domain.User +import java.time.LocalTime + +/** + * ViewModel of a session which has a list of attendances. + */ +class SessionViewModel() : ViewModel() { + + private var _result = MutableLiveData>() + val result: LiveData> = _result + + private var _attendances = MutableLiveData>() + val attendances: LiveData> = _attendances + + // The opened session which have been selected in the menu. + var session: Session? = null + + /** + * Add the [student] in the attendance list [attendances]. + * Return true if it has been added, else return false. + */ + fun addAttendance(student: User) { + val list = _attendances.value ?: emptyList() + + // If the list already contains the user, return false + if (list.any { it.student == student }) { + Log.d("Palto", "SessionViewModel: User already in the list.") + _result.value = Event(SessionResult( + false, + R.string.session_user_already_added, + student.username + )) + + // Else create a new attendance and add it into the list. + } else { + val attendance = Attendance( + id = list.size, + student = student, + date = LocalTime.now() + ) + // Add the attendance in the attendance list, and trigger the observers. + _attendances.value = list + attendance + // Saved the list in the session. + session?.attendances = list + attendance + + _result.value = Event(SessionResult( + true, + R.string.session_user_added, + student.username + )) + Log.d("Palto", "SessionViewModel: An attendance has been added into the list.") + } + } + + fun setAttendanceList(list: List) { + _attendances.value = list + } + + /** + * ViewModel Factory. + */ + companion object { + + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class + ): T { + return SessionViewModel() as T + } + } + } +} diff --git a/app/src/main/java/com/example/palto/ui/session/new_student/NewStudentFragment.kt b/app/src/main/java/com/example/palto/ui/session/new_student/NewStudentFragment.kt new file mode 100644 index 0000000..e52675d --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/session/new_student/NewStudentFragment.kt @@ -0,0 +1,69 @@ +package com.example.palto.ui.session.new_student + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import com.example.palto.NfcViewModel +import com.example.palto.R +import com.example.palto.databinding.FragmentNewSessionBinding +import com.example.palto.databinding.FragmentNewStudentBinding +import com.example.palto.ui.CardViewModel +import com.example.palto.ui.UserViewModel +import com.example.palto.ui.menu.MenuViewModel +import com.example.palto.ui.session.SessionViewModel + +/** + * + */ +class NewStudentFragment : Fragment() { + + private val newStudentViewModel: NewStudentViewModel by viewModels() + + private val sessionViewModel: SessionViewModel by navGraphViewModels(R.id.nav_graph) + + private val userViewModel: UserViewModel by navGraphViewModels(R.id.nav_graph) + + private val cardViewModel: CardViewModel by navGraphViewModels(R.id.nav_graph) + + // This property is only valid between onCreateView and onDestroyView + private lateinit var binding: FragmentNewStudentBinding + + @OptIn(ExperimentalStdlibApi::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentNewStudentBinding.inflate(inflater, container, false) + + // If the bundle value tagId of key "tagId" exists, + // Set it on the screen. + val tagId = arguments?.getString("tagId") + if (tagId != null) { + binding.newStudentCardId.text = tagId + } + + // Bind the create button. + binding.newStudentCreate.setOnClickListener { + val user = userViewModel.createUser(binding.newStudentName.text.toString()) + + // If a tag has been provided, create the card. + // The user would not need to create his account afterward. + if (tagId != null) { + cardViewModel.createCard(user, tagId) + } + + sessionViewModel.addAttendance(user) + findNavController().popBackStack() + } + + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/session/new_student/NewStudentViewModel.kt b/app/src/main/java/com/example/palto/ui/session/new_student/NewStudentViewModel.kt new file mode 100644 index 0000000..8ae9268 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/session/new_student/NewStudentViewModel.kt @@ -0,0 +1,8 @@ +package com.example.palto.ui.session.new_student + +import androidx.lifecycle.ViewModel + +/** + * ViewModel of the session creation form. Used for verifications. + */ +class NewStudentViewModel() : ViewModel() diff --git a/app/src/main/java/com/example/palto/ui/sessionList/SessionListAdapter.kt b/app/src/main/java/com/example/palto/ui/sessionList/SessionListAdapter.kt deleted file mode 100644 index 24f492f..0000000 --- a/app/src/main/java/com/example/palto/ui/sessionList/SessionListAdapter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.palto.ui.sessionList - -import androidx.recyclerview.widget.RecyclerView -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.TextView - -import com.example.palto.ui.sessionList.placeholder.PlaceholderContent.PlaceholderItem -import com.example.palto.databinding.FragmentSessionItemBinding - -/** - * [RecyclerView.Adapter] that can display a [PlaceholderItem]. - */ -class SessionListAdapter(private val values: List) : - RecyclerView.Adapter() { - - class ViewHolder(binding: FragmentSessionItemBinding) : - RecyclerView.ViewHolder(binding.root) { - - val idView: TextView = binding.itemNumber - val contentView: TextView = binding.content - - override fun toString(): String { - return super.toString() + " '" + contentView.text + "'" - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = FragmentSessionItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val item = values[position] - holder.idView.text = item.id - holder.contentView.text = item.content - } - - override fun getItemCount(): Int = values.size -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/sessionList/SessionListFragment.kt b/app/src/main/java/com/example/palto/ui/sessionList/SessionListFragment.kt deleted file mode 100644 index 03424e1..0000000 --- a/app/src/main/java/com/example/palto/ui/sessionList/SessionListFragment.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.palto.ui.sessionList - -import android.os.Bundle -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.example.palto.R -import com.example.palto.ui.sessionList.placeholder.PlaceholderContent - -/** - * A fragment representing a list of Sessions. - */ -class SessionListFragment : Fragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val view = inflater.inflate(R.layout.fragment_session_list, container, false) - - if (view is RecyclerView) { - view.layoutManager = LinearLayoutManager(context) - view.adapter = SessionListAdapter(PlaceholderContent.ITEMS) - } - return view - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/sessionList/SessionListViewModel.kt b/app/src/main/java/com/example/palto/ui/sessionList/SessionListViewModel.kt deleted file mode 100644 index d127a36..0000000 --- a/app/src/main/java/com/example/palto/ui/sessionList/SessionListViewModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.palto.ui.sessionList - -import androidx.lifecycle.ViewModel -import com.example.palto.data.repository.LoginRepository - -class SessionListViewModel(private val loginRepository: LoginRepository) : ViewModel() { - /* - private val _loginForm = MutableLiveData() - val loginFormState: LiveData = _loginForm - - private val _loginResult = MutableLiveData() - val loginResult: LiveData = _loginResult - */ -} \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/ui/sessionList/placeholder/PlaceholderContent.kt b/app/src/main/java/com/example/palto/ui/sessionList/placeholder/PlaceholderContent.kt deleted file mode 100644 index b085b2c..0000000 --- a/app/src/main/java/com/example/palto/ui/sessionList/placeholder/PlaceholderContent.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.palto.ui.sessionList.placeholder - -import java.util.ArrayList -import java.util.HashMap - -/** - * Helper class for providing sample content for user interfaces created by - * Android template wizards. - * - * TODO: Replace all uses of this class before publishing your app. - */ -object PlaceholderContent { - - /** - * An array of sample (placeholder) items. - */ - val ITEMS: MutableList = ArrayList() - - /** - * A map of sample (placeholder) items, by ID. - */ - val ITEM_MAP: MutableMap = HashMap() - - private val COUNT = 25 - - init { - // Add some sample items. - for (i in 1..COUNT) { - addItem(createPlaceholderItem(i)) - } - } - - private fun addItem(item: PlaceholderItem) { - ITEMS.add(item) - ITEM_MAP.put(item.id, item) - } - - private fun createPlaceholderItem(position: Int): PlaceholderItem { - return PlaceholderItem(position.toString(), "Item " + position, makeDetails(position)) - } - - private fun makeDetails(position: Int): String { - val builder = StringBuilder() - builder.append("Details about Item: ").append(position) - for (i in 0..position - 1) { - builder.append("\nMore details information here.") - } - return builder.toString() - } - - /** - * A placeholder item representing a piece of content. - */ - data class PlaceholderItem(val id: String, val content: String, val details: String) { - override fun toString(): String = content - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_palto.xml b/app/src/main/res/layout/activity_palto.xml index 3a70781..cebfacb 100644 --- a/app/src/main/res/layout/activity_palto.xml +++ b/app/src/main/res/layout/activity_palto.xml @@ -1,12 +1,21 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_attendance_list.xml b/app/src/main/res/layout/fragment_attendance_list.xml deleted file mode 100644 index 318836a..0000000 --- a/app/src/main/res/layout/fragment_attendance_list.xml +++ /dev/null @@ -1,13 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index 96f7159..7e7c0c7 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -13,11 +13,8 @@ @@ -77,33 +71,38 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start" - android:layout_marginStart="48dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="48dp" - android:layout_marginBottom="64dp" - android:enabled="false" + android:enabled="true" android:text="@string/action_sign_in" + app:layout_constraintBottom_toTopOf="@+id/login_anonymous" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/login_error" /> + + + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5" /> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/login" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_attendance_item.xml b/app/src/main/res/layout/fragment_menu_item.xml similarity index 61% rename from app/src/main/res/layout/fragment_attendance_item.xml rename to app/src/main/res/layout/fragment_menu_item.xml index b975c33..92c931f 100644 --- a/app/src/main/res/layout/fragment_attendance_item.xml +++ b/app/src/main/res/layout/fragment_menu_item.xml @@ -4,10 +4,17 @@ android:layout_height="wrap_content" android:orientation="horizontal"> - + android:textAppearance="?attr/textAppearanceListItem" />--> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_menu_list.xml b/app/src/main/res/layout/fragment_menu_list.xml new file mode 100644 index 0000000..645676c --- /dev/null +++ b/app/src/main/res/layout/fragment_menu_list.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_new_session.xml b/app/src/main/res/layout/fragment_new_session.xml new file mode 100644 index 0000000..d419ed9 --- /dev/null +++ b/app/src/main/res/layout/fragment_new_session.xml @@ -0,0 +1,33 @@ + + + + + +