diff --git a/.gitignore b/.gitignore index aa724b7..ae8485f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,9 @@ *.iml .gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +local.properties +.idea .DS_Store -/build -/captures +build .externalNativeBuild .cxx local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 7768bde..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Palto \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index ae388c2..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index fdf8d99..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index ac801d8..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2402d4b..39d63cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,14 +30,23 @@ android { jvmTarget = "1.8" } buildToolsVersion = "33.0.1" + buildFeatures { + viewBinding = true + } } 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("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.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") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7f7b9da..fc2cb02 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - @@ -16,14 +17,20 @@ android:theme="@style/Theme.Palto" tools:targetApi="31"> + android:name=".PaltoActivity" + android:exported="true" + android:launchMode="singleTop"> + + + + + diff --git a/app/src/main/java/com/example/palto/MainActivity.kt b/app/src/main/java/com/example/palto/PaltoActivity.kt similarity index 56% rename from app/src/main/java/com/example/palto/MainActivity.kt rename to app/src/main/java/com/example/palto/PaltoActivity.kt index 7936f6c..8a70afa 100644 --- a/app/src/main/java/com/example/palto/MainActivity.kt +++ b/app/src/main/java/com/example/palto/PaltoActivity.kt @@ -1,48 +1,50 @@ package com.example.palto -import android.app.PendingIntent -import android.content.Intent -import android.content.IntentFilter import android.nfc.NfcAdapter import android.nfc.Tag -import android.nfc.tech.IsoDep -import android.nfc.tech.NdefFormatable -import android.nfc.tech.NfcA -import android.os.Build import android.os.Bundle import android.util.Log -import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.activity.viewModels -class MainActivity : AppCompatActivity() { +class PaltoActivity : AppCompatActivity() { + private var nfcAdapter: NfcAdapter? = null + private val paltoViewModel: PaltoViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) // get the NFC Adapter - this.nfcAdapter = NfcAdapter.getDefaultAdapter(this) + nfcAdapter = NfcAdapter.getDefaultAdapter(this) // check if NFC is supported - if (this.nfcAdapter == null) { + if (nfcAdapter == null) { Log.e("NFC", "NFC is not supported") return } // check if NFC is disabled - if (!(this.nfcAdapter!!.isEnabled)) { + 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) + */ } override fun onResume() { super.onResume() - nfcAdapter!!.enableReaderMode( + nfcAdapter?.enableReaderMode( this, - this::processTag, + paltoViewModel.tagLiveData::postValue, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null ) @@ -52,11 +54,11 @@ class MainActivity : AppCompatActivity() { super.onPause() // disable the NFC discovery - this.nfcAdapter!!.disableReaderMode(this) + nfcAdapter?.disableReaderMode(this) } @OptIn(ExperimentalStdlibApi::class) fun processTag(tag: Tag) { - Log.d("NFC", "Tag ID : ${tag.id.toHexString()}") + 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 new file mode 100644 index 0000000..9d69dc4 --- /dev/null +++ b/app/src/main/java/com/example/palto/PaltoViewModel.kt @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..be27f93 --- /dev/null +++ b/app/src/main/java/com/example/palto/data/Result.kt @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..d05487e --- /dev/null +++ b/app/src/main/java/com/example/palto/data/local/LocalDataSource.kt @@ -0,0 +1,24 @@ +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/ServerDataSource.kt b/app/src/main/java/com/example/palto/data/network/ServerDataSource.kt new file mode 100644 index 0000000..8c68892 --- /dev/null +++ b/app/src/main/java/com/example/palto/data/network/ServerDataSource.kt @@ -0,0 +1,60 @@ +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/repository/AttendanceRepository.kt b/app/src/main/java/com/example/palto/data/repository/AttendanceRepository.kt new file mode 100644 index 0000000..13a8631 --- /dev/null +++ b/app/src/main/java/com/example/palto/data/repository/AttendanceRepository.kt @@ -0,0 +1,10 @@ +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 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 new file mode 100644 index 0000000..ad61888 --- /dev/null +++ b/app/src/main/java/com/example/palto/data/repository/LoginRepository.kt @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..3b4b4a7 --- /dev/null +++ b/app/src/main/java/com/example/palto/data/repository/SessionRepository.kt @@ -0,0 +1,10 @@ +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 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 new file mode 100644 index 0000000..184f04c --- /dev/null +++ b/app/src/main/java/com/example/palto/data/repository/TokensRepository.kt @@ -0,0 +1,43 @@ +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/model/Attendance.kt b/app/src/main/java/com/example/palto/model/Attendance.kt new file mode 100644 index 0000000..1288725 --- /dev/null +++ b/app/src/main/java/com/example/palto/model/Attendance.kt @@ -0,0 +1,10 @@ +package com.example.palto.model +import java.io.Serializable + +/** + * Data class that captures tokens for logged in users retrieved from LoginRepository + */ +data class Attendance( + val date: String, + val access: String +) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/example/palto/model/Card.kt b/app/src/main/java/com/example/palto/model/Card.kt new file mode 100644 index 0000000..8c55422 --- /dev/null +++ b/app/src/main/java/com/example/palto/model/Card.kt @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..6f60811 --- /dev/null +++ b/app/src/main/java/com/example/palto/model/LoggedInUser.kt @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..8a6827f --- /dev/null +++ b/app/src/main/java/com/example/palto/model/Session.kt @@ -0,0 +1,9 @@ +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/model/Tokens.kt b/app/src/main/java/com/example/palto/model/Tokens.kt new file mode 100644 index 0000000..fa90475 --- /dev/null +++ b/app/src/main/java/com/example/palto/model/Tokens.kt @@ -0,0 +1,10 @@ +package com.example.palto.model +import java.io.Serializable + +/** + * Data class that captures tokens for logged in users retrieved from LoginRepository + */ +data class Tokens( + val refresh: String, + val access: String +) : Serializable \ 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 new file mode 100644 index 0000000..a7788b1 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListAdapter.kt @@ -0,0 +1,58 @@ +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 new file mode 100644 index 0000000..68773ab --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListFragment.kt @@ -0,0 +1,69 @@ +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 new file mode 100644 index 0000000..a4efa38 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/attendanceList/AttendanceListViewModel.kt @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..8cddb61 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/login/LoggedInUserView.kt @@ -0,0 +1,13 @@ +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/LoginFormState.kt b/app/src/main/java/com/example/palto/ui/login/LoginFormState.kt new file mode 100644 index 0000000..8edce28 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/login/LoginFormState.kt @@ -0,0 +1,11 @@ +package com.example.palto.ui.login + +/** + * Data validation state of the login form. + */ +data class LoginFormState( + val hostnameError: Int? = null, + val usernameError: Int? = null, + val passwordError: Int? = null, + val isDataValid: Boolean = false +) \ 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 new file mode 100644 index 0000000..acf12be --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/login/LoginFragment.kt @@ -0,0 +1,136 @@ +package com.example.palto.ui.login + +import androidx.lifecycle.Observer +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +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.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() } + + private var _binding: FragmentLoginBinding? = null + + // This property is only valid between onCreateView and onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + _binding = FragmentLoginBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val hostnameEditText = binding.hostname + val usernameEditText = binding.username + val passwordEditText = binding.password + 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.loginResult.observe(viewLifecycleOwner, + Observer { loginResult -> + loginResult ?: return@Observer + loadingProgressBar.visibility = View.GONE + loginResult.error?.let { + showLoginFailed(it) + } + loginResult.success?.let { + //findNavController().navigate(R.id.action_loginFragment_to_attendanceFragment) + //updateUiWithUser(it) + } + }) + + val afterTextChangedListener = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { } + + override fun afterTextChanged(s: Editable) { + loginViewModel.loginDataChanged( + hostnameEditText.text.toString(), + usernameEditText.text.toString(), + passwordEditText.text.toString() + ) + } + } + hostnameEditText.addTextChangedListener(afterTextChangedListener) + usernameEditText.addTextChangedListener(afterTextChangedListener) + passwordEditText.addTextChangedListener(afterTextChangedListener) + passwordEditText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + loginViewModel.login( + hostnameEditText.text.toString(), + usernameEditText.text.toString(), + passwordEditText.text.toString() + ) + } + 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) { + val welcome = getString(R.string.welcome) + model.displayName + // TODO : initiate successful logged in experience + 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 new file mode 100644 index 0000000..9367f89 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/login/LoginResult.kt @@ -0,0 +1,9 @@ +package com.example.palto.ui.login + +/** + * Authentication result : success (user details) or error message. + */ +data class LoginResult( + val success: LoggedInUserView? = null, + val error: Int? = 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 new file mode 100644 index 0000000..99ea964 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/login/LoginViewModel.kt @@ -0,0 +1,84 @@ +package com.example.palto.ui.login + +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 com.example.palto.R +import com.example.palto.data.network.ServerDataSource + +class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { + + private val _loginForm = MutableLiveData() + val loginFormState: LiveData = _loginForm + + private val _loginResult = MutableLiveData() + val loginResult: LiveData = _loginResult + + 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() + ) + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} 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 new file mode 100644 index 0000000..24f492f --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/sessionList/SessionListAdapter.kt @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..03424e1 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/sessionList/SessionListFragment.kt @@ -0,0 +1,31 @@ +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 new file mode 100644 index 0000000..d127a36 --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/sessionList/SessionListViewModel.kt @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..b085b2c --- /dev/null +++ b/app/src/main/java/com/example/palto/ui/sessionList/placeholder/PlaceholderContent.kt @@ -0,0 +1,57 @@ +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_main.xml b/app/src/main/res/layout/activity_palto.xml similarity index 55% rename from app/src/main/res/layout/activity_main.xml rename to app/src/main/res/layout/activity_palto.xml index 1b3bb02..3a70781 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_palto.xml @@ -3,17 +3,17 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".MainActivity"> + android:layout_height="match_parent"> - - + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/nav_graph" /> \ 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_attendance_item.xml new file mode 100644 index 0000000..b975c33 --- /dev/null +++ b/app/src/main/res/layout/fragment_attendance_item.xml @@ -0,0 +1,13 @@ + + + + + \ 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 new file mode 100644 index 0000000..318836a --- /dev/null +++ b/app/src/main/res/layout/fragment_attendance_list.xml @@ -0,0 +1,13 @@ + + \ 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 new file mode 100644 index 0000000..96f7159 --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + +