diff --git a/app/src/main/java/com/example/palto/NfcViewModel.kt b/app/src/main/java/com/example/palto/NfcViewModel.kt new file mode 100644 index 0000000..32e4d7e --- /dev/null +++ b/app/src/main/java/com/example/palto/NfcViewModel.kt @@ -0,0 +1,22 @@ +package com.example.palto + +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.Event +import com.example.palto.domain.User + +class NfcViewModel: ViewModel() { + + private var _tagId = MutableLiveData>() + 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 5fb5810..02773bd 100644 --- a/app/src/main/java/com/example/palto/PaltoActivity.kt +++ b/app/src/main/java/com/example/palto/PaltoActivity.kt @@ -11,8 +11,7 @@ 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.NfcViewModel -import com.example.palto.ui.UserViewModel +import com.example.palto.ui.CardViewModel class PaltoActivity : AppCompatActivity() { @@ -21,8 +20,6 @@ class PaltoActivity : AppCompatActivity() { private val nfcViewModel: NfcViewModel by viewModels() - private val userViewModel: UserViewModel by viewModels() { UserViewModel.Factory } - private lateinit var binding: ActivityPaltoBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -81,7 +78,7 @@ class PaltoActivity : AppCompatActivity() { // Begin to read NFC Cards. nfcAdapter?.enableReaderMode( this, - nfcViewModel.tag::postValue, + nfcViewModel::setTag, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ) diff --git a/app/src/main/java/com/example/palto/domain/Attendance.kt b/app/src/main/java/com/example/palto/domain/Attendance.kt index 356ac19..1d6c321 100644 --- a/app/src/main/java/com/example/palto/domain/Attendance.kt +++ b/app/src/main/java/com/example/palto/domain/Attendance.kt @@ -1,10 +1,12 @@ 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 index 4683d05..0520b42 100644 --- a/app/src/main/java/com/example/palto/domain/Card.kt +++ b/app/src/main/java/com/example/palto/domain/Card.kt @@ -1,22 +1,7 @@ package com.example.palto.domain 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 + 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 index e574b59..4e2116e 100644 --- a/app/src/main/java/com/example/palto/domain/Session.kt +++ b/app/src/main/java/com/example/palto/domain/Session.kt @@ -7,5 +7,6 @@ import java.io.Serializable data class Session( val id: Int, val name: String, - var attendances: List + 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/domain/User.kt b/app/src/main/java/com/example/palto/domain/User.kt index 60fd4b2..7e36833 100644 --- a/app/src/main/java/com/example/palto/domain/User.kt +++ b/app/src/main/java/com/example/palto/domain/User.kt @@ -4,10 +4,9 @@ package com.example.palto.domain * Data class that captures user information for logged in users retrieved from LoginRepository */ data class User( - //Not in the domain layer - //val id: String, + val id: Int, val username: String, - val first_name: String, - val last_name: String, + val firstName: String, + val lastName: String, val email: String ) 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/NfcViewModel.kt b/app/src/main/java/com/example/palto/ui/NfcViewModel.kt deleted file mode 100644 index 058fc7b..0000000 --- a/app/src/main/java/com/example/palto/ui/NfcViewModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.palto.ui - -import android.nfc.Tag -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.example.palto.domain.User - -class NfcViewModel: ViewModel() { - val tag = MutableLiveData() - - private var _user : MutableLiveData = MutableLiveData() - val user : LiveData = _user -} \ 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 index a767420..ac791c4 100644 --- a/app/src/main/java/com/example/palto/ui/UserViewModel.kt +++ b/app/src/main/java/com/example/palto/ui/UserViewModel.kt @@ -4,75 +4,28 @@ import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.example.palto.R -import com.example.palto.data.repository.TokenRepository -import com.example.palto.data.repository.UserRepository +import com.example.palto.domain.Card import com.example.palto.domain.User -import com.example.palto.ui.login.LoginResult -import kotlinx.coroutines.launch -class UserViewModel( - private val tokenRepository: TokenRepository, - private val userRepository: UserRepository -): ViewModel() { +/** + * UserViewModel maintain a list of users application wide. + * May be converted into a repository. + */ +class UserViewModel: ViewModel() { - private var _result = MutableLiveData() - val result = _result as LiveData + private var _users = MutableLiveData>() + val users : LiveData> = _users - // User is initially set to null to be disconnected. - private var _user = MutableLiveData(null) - val user = _user as LiveData - - fun login( - hostname: String, - username: String, - password: String) { - - // Coroutine runs in background. - viewModelScope.launch { - try { - tokenRepository.authenticate(hostname, username, password) - _user.value = User( - 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 - ) - } - } - } - - fun loginAnonymous() { - _user.value = User( - "anonymous", "", "", "" - ) - _result.value = LoginResult(success = true) - } - - fun logout() { - _user.value = null - _result.value = LoginResult(success = false) - } - - companion object { - - val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return UserViewModel( - tokenRepository = TokenRepository(), - userRepository = UserRepository() - ) as T - } - } + 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/login/LoginFragment.kt b/app/src/main/java/com/example/palto/ui/login/LoginFragment.kt index 85530cf..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 @@ -8,22 +8,16 @@ import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.example.palto.databinding.FragmentLoginBinding -import com.example.palto.ui.UserViewModel class LoginFragment : Fragment() { - // loginViewModel is used to update the login screen dynamically. - private val loginViewModel: LoginViewModel by viewModels() - // userViewModel is where the user is logged in, at the activity level. - private val userViewModel: UserViewModel by activityViewModels() { UserViewModel.Factory } + private val loginViewModel: LoginViewModel by activityViewModels() { LoginViewModel.Factory } private lateinit var binding: FragmentLoginBinding - @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -36,7 +30,7 @@ class LoginFragment : Fragment() { // Bind the login button. binding.login.setOnClickListener { binding.loading.visibility = View.VISIBLE - userViewModel.login( + loginViewModel.login( binding.hostname.text.toString(), binding.username.text.toString(), binding.password.text.toString() @@ -45,16 +39,16 @@ class LoginFragment : Fragment() { // Bind anonymous login clickable text. binding.loginAnonymous.setOnClickListener { - userViewModel.loginAnonymous() + loginViewModel.loginAnonymous() } // On result of logging. - userViewModel.result.observe(viewLifecycleOwner) { + 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() + binding.loginError.text = "Exception : ${it.exception.toString()}" Toast.makeText(activity, it.error, Toast.LENGTH_LONG).show() } } 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 1e79ad5..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,41 +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 androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.example.palto.R +import com.example.palto.data.repository.TokenRepository +import com.example.palto.data.repository.UserRepository +import com.example.palto.domain.User +import kotlinx.coroutines.launch /** - * View Model to check dynamically the values of the form. + * LoginViewModel to access information about the logged in user and login form. */ -class LoginViewModel() : ViewModel() { +class LoginViewModel( + private val tokenRepository: TokenRepository, + private val userRepository: UserRepository +): ViewModel() { + + private var _result = MutableLiveData() + val result = _result as LiveData + + // 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 loginDataChanged( + fun login( 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) + + // 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 + ) + } } } + + 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() + return hostname.isNotBlank() } private fun isUserNameValid(username: String): Boolean { - return username.isNotBlank() + return username.isNotBlank() } private fun isPasswordValid(password: String): Boolean { - return password.length > 5 + 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/MenuFragment.kt b/app/src/main/java/com/example/palto/ui/menu/MenuFragment.kt index 6aceecc..2991f91 100644 --- a/app/src/main/java/com/example/palto/ui/menu/MenuFragment.kt +++ b/app/src/main/java/com/example/palto/ui/menu/MenuFragment.kt @@ -12,7 +12,7 @@ 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.UserViewModel +import com.example.palto.ui.login.LoginViewModel /** * A fragment representing a list of Sessions. @@ -22,8 +22,8 @@ class MenuFragment : Fragment() { private val menuViewModel: MenuViewModel by navGraphViewModels(R.id.nav_graph) { MenuViewModel.Factory } - private val userViewModel: UserViewModel by - activityViewModels() { UserViewModel.Factory } + private val loginViewModel: LoginViewModel by + activityViewModels() { LoginViewModel.Factory } // This property is only valid between onCreateView and onDestroyView private lateinit var binding: FragmentMenuListBinding @@ -38,7 +38,7 @@ class MenuFragment : Fragment() { val navController = findNavController() // Connect the user. - userViewModel.user.observe(viewLifecycleOwner) { + loginViewModel.user.observe(viewLifecycleOwner) { if (it != null) { // Get sessions of the user from remote. } else { 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 index b67bd65..e263c6d 100644 --- a/app/src/main/java/com/example/palto/ui/menu/MenuViewModel.kt +++ b/app/src/main/java/com/example/palto/ui/menu/MenuViewModel.kt @@ -5,14 +5,16 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.example.palto.data.repository.TokenRepository -import com.example.palto.data.repository.UserRepository +import com.example.palto.domain.Attendance import com.example.palto.domain.Session -import com.example.palto.ui.UserViewModel +/** + * ViewModel for accessing all the sessions created. + */ class MenuViewModel() : ViewModel() { - private var _sessions = MutableLiveData>(emptyList()) + // A list of sessions. + private var _sessions = MutableLiveData>() val sessions: LiveData> = _sessions fun createSession(name: String) { @@ -26,6 +28,14 @@ class MenuViewModel() : ViewModel() { 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 { 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 index 6a11a69..afb2fd0 100644 --- a/app/src/main/java/com/example/palto/ui/session/SessionAdapter.kt +++ b/app/src/main/java/com/example/palto/ui/session/SessionAdapter.kt @@ -7,47 +7,46 @@ 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 [Card] items. + * A [ListAdapter] that can display [Attendance] items. */ -class SessionAdapter : - ListAdapter(CardDiffCallback) { +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 { - return ViewHolder( - FragmentSessionItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) + 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.cardId.text = item.uid.toHexString() - } - - inner class ViewHolder( - binding: FragmentSessionItemBinding - ) : RecyclerView.ViewHolder(binding.root) { - - val cardId: TextView = binding.cardId - override fun toString(): String { - return super.toString() + " '" + cardId.text + "'" - } + holder.bind(item) } } -object CardDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Card, newItem: Card): Boolean { +object AttendanceDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Attendance, newItem: Attendance): Boolean { return oldItem == newItem } - override fun areContentsTheSame(oldItem: Card, newItem: Card): Boolean { + 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 index 1f19919..35e7e82 100644 --- a/app/src/main/java/com/example/palto/ui/session/SessionFragment.kt +++ b/app/src/main/java/com/example/palto/ui/session/SessionFragment.kt @@ -5,12 +5,18 @@ 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.ui.NfcViewModel +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. @@ -20,12 +26,16 @@ class SessionFragment : Fragment() { private val sessionViewModel: SessionViewModel by navGraphViewModels(R.id.nav_graph) { SessionViewModel.Factory } - private val nfcViewModel: NfcViewModel by - activityViewModels() + 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() - private var _binding: FragmentSessionListBinding? = null // This property is only valid between onCreateView and onDestroyView - private val binding get() = _binding!! + private lateinit var binding: FragmentSessionListBinding /** * Have the fragment instantiate the user interface. @@ -35,25 +45,62 @@ class SessionFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - _binding = FragmentSessionListBinding.inflate(inflater, container, false) + 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.cards.observe(viewLifecycleOwner) { + sessionViewModel.attendances.observe(viewLifecycleOwner) { adapter.submitList(it) } // Set the listener for a new NFC tag. - nfcViewModel.tag.observe(viewLifecycleOwner) { - Log.d("NFC", "The tag observers has been notified.") - sessionViewModel.insertCard(it) + 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) + } + } } - binding.createCard.setOnClickListener { - //sessionViewModel. + // 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 index f9e4c61..eb81aa5 100644 --- a/app/src/main/java/com/example/palto/ui/session/SessionViewModel.kt +++ b/app/src/main/java/com/example/palto/ui/session/SessionViewModel.kt @@ -1,37 +1,74 @@ package com.example.palto.ui.session -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.repository.AttendanceRepository -import com.example.palto.domain.Card +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( - private val attendanceRepository: AttendanceRepository -) : ViewModel() { +class SessionViewModel() : ViewModel() { - private val _cards = MutableLiveData>(emptyList()) - val cards = _cards as LiveData> + private var _result = MutableLiveData>() + val result: LiveData> = _result - fun insertCard(tag: Tag) { - val card = Card( - "0", - tag.id, - "tmp", - "tmp" - ) - _cards.value = (_cards.value ?: emptyList()) + card - Log.d("PALTO", "SessionViewModel: a card has been added into the list.") + 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. + * ViewModel Factory. */ companion object { @@ -40,9 +77,7 @@ class SessionViewModel( override fun create( modelClass: Class ): T { - return SessionViewModel( - AttendanceRepository() - ) as 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/res/layout/fragment_new_student.xml b/app/src/main/res/layout/fragment_new_student.xml new file mode 100644 index 0000000..c4e2eb2 --- /dev/null +++ b/app/src/main/res/layout/fragment_new_student.xml @@ -0,0 +1,44 @@ + + + + + + + +