Ajout de la connexion d’un utilisateur au serveur Palto et connexion anonyme hors ligne.y

This commit is contained in:
biloute02 2024-01-11 22:21:31 +01:00
parent 69d1880295
commit e81f70f045
17 changed files with 207 additions and 310 deletions

View file

@ -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<Tag>()
}

View file

@ -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<LoggedInUser> {
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
}
*/
}

View file

@ -1,10 +1,3 @@
package com.example.palto.data.repository package com.example.palto.data.repository
import com.example.palto.data.network.PaltoApiService class AttendanceRepository
/**
*
*/
class AttendanceRepository() {
// private val cards
}

View file

@ -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<LoggedInUser> {
// 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
}
}

View file

@ -1,10 +1,3 @@
package com.example.palto.data.repository package com.example.palto.data.repository
import com.example.palto.data.network.PaltoApiService class SessionRepository
/**
*
*/
class SessionRepository(val dataSource: PaltoApiService) {
// private val cards
}

View file

@ -0,0 +1,3 @@
package com.example.palto.data.repository
class UserRepository

View file

@ -1,14 +1,13 @@
package com.example.palto.model package com.example.palto.domain
import java.io.Serializable
/** /**
* Data class that captures user information for logged in users retrieved from LoginRepository * Data class that captures user information for logged in users retrieved from LoginRepository
*/ */
data class LoggedInUser( data class User(
val id: String, //Not in the domain layer
//val id: String,
val username: String, val username: String,
val first_name: String, val first_name: String,
val last_name: String, val last_name: String,
val email: String val email: String
) : Serializable )

View file

@ -0,0 +1,78 @@
package com.example.palto.ui
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 com.example.palto.ui.login.LoginResult
import kotlinx.coroutines.launch
class UserViewModel(
private val tokenRepository: TokenRepository,
private val userRepository: UserRepository
): ViewModel() {
private var _result = MutableLiveData<LoginResult>()
val result = _result as LiveData<LoginResult>
// User is initially set to null to be disconnected.
private var _user = MutableLiveData<User?>(null)
val user = _user as LiveData<User?>
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 <T : ViewModel> create(modelClass: Class<T>): T {
return UserViewModel(
tokenRepository = TokenRepository(),
userRepository = UserRepository()
) as T
}
}
}
}

View file

@ -1,13 +0,0 @@
package com.example.palto.ui.login
/* Est-ce que cest 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
)

View file

@ -1,42 +1,68 @@
package com.example.palto.ui.login package com.example.palto.ui.login
import androidx.lifecycle.Observer import android.annotation.SuppressLint
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.Toast 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 androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.example.palto.databinding.FragmentLoginBinding import com.example.palto.databinding.FragmentLoginBinding
import com.example.palto.ui.UserViewModel
import com.example.palto.R
class LoginFragment : Fragment() { class LoginFragment : Fragment() {
private val loginViewModel: LoginViewModel by // loginViewModel is used to update the login screen dynamically.
navGraphViewModels(R.id.nav_graph) { LoginViewModelFactory() } private val loginViewModel: LoginViewModel by viewModels()
private var _binding: FragmentLoginBinding? = null // userViewModel is where the user is logged in, at the activity level.
private val userViewModel: UserViewModel by activityViewModels() { UserViewModel.Factory }
// This property is only valid between onCreateView and onDestroyView. private lateinit var binding: FragmentLoginBinding
private val binding get() = _binding!!
@SuppressLint("SetTextI18n")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
binding = FragmentLoginBinding.inflate(inflater, container, false)
val navController = findNavController()
// Bind the login button.
binding.login.setOnClickListener {
binding.loading.visibility = View.VISIBLE
userViewModel.login(
binding.hostname.text.toString(),
binding.username.text.toString(),
binding.password.text.toString()
)
}
// Bind anonymous login clickable text.
binding.loginAnonymous.setOnClickListener {
userViewModel.loginAnonymous()
}
// On result of logging.
userViewModel.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 return binding.root
} }
/*
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -46,23 +72,21 @@ class LoginFragment : Fragment() {
val loginButton = binding.login val loginButton = binding.login
val loadingProgressBar = binding.loading val loadingProgressBar = binding.loading
// loginViewModel.loginFormState.observe(viewLifecycleOwner) {
loginViewModel.loginFormState.observe(viewLifecycleOwner, if (it == null) {
Observer { loginFormState -> return@Observer
if (loginFormState == null) { }
return@Observer loginButton.isEnabled = it.isDataValid
} it.hostnameError?.let {
loginButton.isEnabled = loginFormState.isDataValid hostnameEditText.error = getString(it)
loginFormState.hostnameError?.let { }
hostnameEditText.error = getString(it) it.usernameError?.let {
} usernameEditText.error = getString(it)
loginFormState.usernameError?.let { }
usernameEditText.error = getString(it) it.passwordError?.let {
} passwordEditText.error = getString(it)
loginFormState.passwordError?.let { }
passwordEditText.error = getString(it) }
}
})
loginViewModel.loginResult.observe(viewLifecycleOwner, loginViewModel.loginResult.observe(viewLifecycleOwner,
Observer { loginResult -> Observer { loginResult ->
@ -103,17 +127,8 @@ class LoginFragment : Fragment() {
} }
false 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) { private fun updateUiWithUser(model: LoggedInUserView) {
@ -122,15 +137,11 @@ class LoginFragment : Fragment() {
val appContext = context?.applicationContext ?: return val appContext = context?.applicationContext ?: return
Toast.makeText(appContext, welcome, Toast.LENGTH_LONG).show() Toast.makeText(appContext, welcome, Toast.LENGTH_LONG).show()
} }
*/
private fun showLoginFailed(@StringRes errorString: Int) { private fun showLoginFailed(@StringRes errorString: Int) {
val appContext = context?.applicationContext ?: return val appContext = context?.applicationContext ?: return
Toast.makeText(appContext, errorString, Toast.LENGTH_LONG).show() Toast.makeText(appContext, errorString, Toast.LENGTH_LONG).show()
} }
override fun onDestroyView() { */
super.onDestroyView()
_binding = null
}
} }

View file

@ -1,9 +1,10 @@
package com.example.palto.ui.login 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( data class LoginResult(
val success: LoggedInUserView? = null, val success: Boolean,
val error: Int? = null val error: Int? = null, // Id of the string resource to display to the user.
val exception: Exception? = null
) )

View file

@ -1,40 +1,14 @@
package com.example.palto.ui.login package com.example.palto.ui.login
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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 * View Model to check dynamically the values of the form.
*/
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { class LoginViewModel() : ViewModel() {
/*
private val _loginForm = MutableLiveData<LoginFormState>() private val _loginFormState = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginForm val loginFormState: LiveData<LoginFormState> = _loginFormState
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _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( fun loginDataChanged(
hostname: String, hostname: String,
@ -50,35 +24,18 @@ class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel()
_loginForm.value = LoginFormState(isDataValid = true) _loginForm.value = LoginFormState(isDataValid = true)
} }
} }
private fun isHostNameValid(hostname: String): Boolean { private fun isHostNameValid(hostname: String): Boolean {
return hostname.isNotBlank() return hostname.isNotBlank()
} }
// A placeholder username validation check
private fun isUserNameValid(username: String): Boolean { private fun isUserNameValid(username: String): Boolean {
return if (username.contains("@")) { return username.isNotBlank()
Patterns.EMAIL_ADDRESS.matcher(username).matches()
} else {
username.isNotBlank()
}
} }
// A placeholder password validation check
private fun isPasswordValid(password: String): Boolean { private fun isPasswordValid(password: String): Boolean {
return password.length > 5 return password.length > 5
} }
*/
} }
class LoginViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
return LoginViewModel(
loginRepository = LoginRepository(
dataSource = ServerDataSource()
)
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -27,8 +27,20 @@ class MenuFragment : Fragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
_binding = FragmentMenuListBinding.inflate(inflater, container, false) binding = FragmentMenuListBinding.inflate(inflater, container, false)
val navController = findNavController()
// Connect the user.
userViewModel.user.observe(viewLifecycleOwner) {
if (it != null) {
// Get sessions of the user from remote.
} else {
navController.navigate(R.id.loginFragment)
}
}
// Display the list of sessions.
val adapter = MenuAdapter() val adapter = MenuAdapter()
binding.menuList.adapter = adapter binding.menuList.adapter = adapter
menuViewModel.sessions.observe(viewLifecycleOwner) { menuViewModel.sessions.observe(viewLifecycleOwner) {

View file

@ -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<PlaceholderItem> = ArrayList()
/**
* A map of sample (placeholder) items, by ID.
*/
val ITEM_MAP: MutableMap<String, PlaceholderItem> = 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
}
}

View file

@ -13,11 +13,8 @@
<EditText <EditText
android:id="@+id/hostname" android:id="@+id/hostname"
android:layout_width="0dp" android:layout_width="331dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="24dp"
android:autofillHints="@string/prompt_hostname" android:autofillHints="@string/prompt_hostname"
android:hint="@string/prompt_hostname" android:hint="@string/prompt_hostname"
android:inputType="text" android:inputType="text"
@ -30,10 +27,8 @@
<EditText <EditText
android:id="@+id/username" android:id="@+id/username"
android:layout_width="0dp" android:layout_width="331dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:autofillHints="@string/prompt_username" android:autofillHints="@string/prompt_username"
android:hint="@string/prompt_username" android:hint="@string/prompt_username"
android:inputType="text" android:inputType="text"
@ -46,29 +41,28 @@
<EditText <EditText
android:id="@+id/password" android:id="@+id/password"
android:layout_width="0dp" android:layout_width="331dp"
android:layout_height="wrap_content" android:layout_height="50dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:autofillHints="@string/prompt_password" android:autofillHints="@string/prompt_password"
android:hint="@string/prompt_password" android:hint="@string/prompt_password"
android:imeActionLabel="@string/action_sign_in_short" android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:inputType="textPassword" android:inputType="textPassword"
android:selectAllOnFocus="true" android:selectAllOnFocus="true"
app:layout_constraintBottom_toTopOf="@+id/help_message" app:layout_constraintBottom_toTopOf="@+id/login_error"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" /> app:layout_constraintTop_toBottomOf="@+id/username" />
<TextView <TextView
android:id="@+id/help_message" android:id="@+id/login_error"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/help_message" android:text=""
app:layout_constraintBottom_toTopOf="@+id/login" app:layout_constraintBottom_toTopOf="@+id/login"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password" /> app:layout_constraintTop_toBottomOf="@+id/password" />
@ -77,33 +71,38 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start" android:layout_gravity="start"
android:layout_marginStart="48dp" android:enabled="true"
android:layout_marginTop="8dp"
android:layout_marginEnd="48dp"
android:layout_marginBottom="64dp"
android:enabled="false"
android:text="@string/action_sign_in" 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" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="39dp"
android:layout_height="47dp"
android:layout_gravity="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/help_message" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.2" /> app:layout_constraintVertical_bias="0.5" />
<ProgressBar <TextView
android:id="@+id/loading" android:id="@+id/login_anonymous"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:clickable="true"
android:layout_marginStart="32dp" android:text="@string/login_anonymous_text"
android:layout_marginTop="64dp" android:textColor="#F4511E"
android:layout_marginEnd="32dp"
android:layout_marginBottom="64dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/password" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/password" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="0.3" /> app:layout_constraintTop_toBottomOf="@+id/login" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -5,6 +5,7 @@
<string name="prompt_username">Nom dutilisateur</string> <string name="prompt_username">Nom dutilisateur</string>
<string name="prompt_password">Mot de passe</string> <string name="prompt_password">Mot de passe</string>
<string name="action_sign_in">Connexion</string> <string name="action_sign_in">Connexion</string>
<string name="login_anonymous_text">Connexion anonyme</string>
<string name="action_sign_in_short">Sign in</string> <string name="action_sign_in_short">Sign in</string>
<string name="welcome">"Bienvenue !"</string> <string name="welcome">"Bienvenue !"</string>
<string name="invalid_hostname">Serveur inaccessible</string> <string name="invalid_hostname">Serveur inaccessible</string>

View file

@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "8.2.0" apply false id("com.android.application") version "8.2.1" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("org.jetbrains.kotlin.android") version "1.9.0" apply false
} }