commit
46de8a3db5
46 changed files with 1122 additions and 88 deletions
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -1,15 +1,9 @@
|
||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
local.properties
|
||||||
/.idea/caches
|
.idea
|
||||||
/.idea/libraries
|
|
||||||
/.idea/modules.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/navEditor.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
build
|
||||||
/captures
|
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
|
|
3
.idea/.gitignore
vendored
3
.idea/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
|
@ -1 +0,0 @@
|
||||||
Palto
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="17" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
|
||||||
<component name="GradleSettings">
|
|
||||||
<option name="linkedExternalProjectsSettings">
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="testRunner" value="GRADLE" />
|
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
|
||||||
<option name="gradleJvm" value="jbr-17" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$" />
|
|
||||||
<option value="$PROJECT_DIR$/app" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="KotlinJpsPluginSettings">
|
|
||||||
<option name="version" value="1.9.0" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -30,14 +30,23 @@ android {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
buildToolsVersion = "33.0.1"
|
buildToolsVersion = "33.0.1"
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("com.google.android.material:material:1.10.0")
|
implementation("com.google.android.material:material:1.10.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
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")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
|
@ -16,14 +17,20 @@
|
||||||
android:theme="@style/Theme.Palto"
|
android:theme="@style/Theme.Palto"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".PaltoActivity"
|
||||||
android:exported="true" >
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
|
@ -1,48 +1,50 @@
|
||||||
package com.example.palto
|
package com.example.palto
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.nfc.NfcAdapter
|
import android.nfc.NfcAdapter
|
||||||
import android.nfc.Tag
|
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.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class PaltoActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var nfcAdapter: NfcAdapter? = null
|
private var nfcAdapter: NfcAdapter? = null
|
||||||
|
|
||||||
|
private val paltoViewModel: PaltoViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
|
|
||||||
// get the NFC Adapter
|
// get the NFC Adapter
|
||||||
this.nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||||
|
|
||||||
// check if NFC is supported
|
// check if NFC is supported
|
||||||
if (this.nfcAdapter == null) {
|
if (nfcAdapter == null) {
|
||||||
Log.e("NFC", "NFC is not supported")
|
Log.e("NFC", "NFC is not supported")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if NFC is disabled
|
// check if NFC is disabled
|
||||||
if (!(this.nfcAdapter!!.isEnabled)) {
|
if (nfcAdapter?.isEnabled == false) {
|
||||||
Log.w("NFC", "NFC is not enabled")
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
nfcAdapter!!.enableReaderMode(
|
nfcAdapter?.enableReaderMode(
|
||||||
this,
|
this,
|
||||||
this::processTag,
|
paltoViewModel.tagLiveData::postValue,
|
||||||
NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
|
NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
@ -52,11 +54,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
// disable the NFC discovery
|
// disable the NFC discovery
|
||||||
this.nfcAdapter!!.disableReaderMode(this)
|
nfcAdapter?.disableReaderMode(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
fun processTag(tag: Tag) {
|
fun processTag(tag: Tag) {
|
||||||
Log.d("NFC", "Tag ID : ${tag.id.toHexString()}")
|
Log.d("NFC", "Tag ID : " + tag.id.toHexString())
|
||||||
}
|
}
|
||||||
}
|
}
|
9
app/src/main/java/com/example/palto/PaltoViewModel.kt
Normal file
9
app/src/main/java/com/example/palto/PaltoViewModel.kt
Normal file
|
@ -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<Tag>()
|
||||||
|
}
|
18
app/src/main/java/com/example/palto/data/Result.kt
Normal file
18
app/src/main/java/com/example/palto/data/Result.kt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package com.example.palto.data
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic class that holds a value with its loading status.
|
||||||
|
* @param <T>
|
||||||
|
*/
|
||||||
|
sealed class Result<out T : Any> {
|
||||||
|
|
||||||
|
data class Success<out T : Any>(val data: T) : Result<T>()
|
||||||
|
data class Error(val exception: Exception) : Result<Nothing>()
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (this) {
|
||||||
|
is Success<*> -> "Success[data=$data]"
|
||||||
|
is Error -> "Error[exception=$exception]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<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
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
|
@ -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<Tokens> {
|
||||||
|
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<Tokens> {
|
||||||
|
return Result.Success(current_tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyToken(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(
|
||||||
|
hostname: String,
|
||||||
|
username: String,
|
||||||
|
password: String
|
||||||
|
): Result<LoggedInUser> {
|
||||||
|
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() { }
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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<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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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<LoggedInUser> {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
10
app/src/main/java/com/example/palto/model/Attendance.kt
Normal file
10
app/src/main/java/com/example/palto/model/Attendance.kt
Normal file
|
@ -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
|
23
app/src/main/java/com/example/palto/model/Card.kt
Normal file
23
app/src/main/java/com/example/palto/model/Card.kt
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
app/src/main/java/com/example/palto/model/LoggedInUser.kt
Normal file
14
app/src/main/java/com/example/palto/model/LoggedInUser.kt
Normal file
|
@ -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
|
9
app/src/main/java/com/example/palto/model/Session.kt
Normal file
9
app/src/main/java/com/example/palto/model/Session.kt
Normal file
|
@ -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
|
10
app/src/main/java/com/example/palto/model/Tokens.kt
Normal file
10
app/src/main/java/com/example/palto/model/Tokens.kt
Normal file
|
@ -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
|
|
@ -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<Card, AttendanceListAdapter.ViewHolder>(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<Card>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Card, newItem: Card): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Card, newItem: Card): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<List<Card>> = 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 <T : ViewModel> create(
|
||||||
|
modelClass: Class<T>
|
||||||
|
): T {
|
||||||
|
return AttendanceListViewModel(
|
||||||
|
AttendanceRepository(ServerDataSource())
|
||||||
|
) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
136
app/src/main/java/com/example/palto/ui/login/LoginFragment.kt
Normal file
136
app/src/main/java/com/example/palto/ui/login/LoginFragment.kt
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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<LoginFormState>()
|
||||||
|
val loginFormState: LiveData<LoginFormState> = _loginForm
|
||||||
|
|
||||||
|
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(
|
||||||
|
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 <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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<PlaceholderItem>) :
|
||||||
|
RecyclerView.Adapter<SessionListAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<LoginFormState>()
|
||||||
|
val loginFormState: LiveData<LoginFormState> = _loginForm
|
||||||
|
|
||||||
|
private val _loginResult = MutableLiveData<LoginResult>()
|
||||||
|
val loginResult: LiveData<LoginResult> = _loginResult
|
||||||
|
*/
|
||||||
|
}
|
|
@ -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<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
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,17 +3,17 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<TextView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/test_text"
|
android:id="@+id/fragmentContainerView"
|
||||||
android:layout_width="wrap_content"
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:text="Hello World!"
|
android:layout_height="0dp"
|
||||||
|
app:defaultNavHost="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:navGraph="@navigation/nav_graph" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
13
app/src/main/res/layout/fragment_attendance_item.xml
Normal file
13
app/src/main/res/layout/fragment_attendance_item.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/card_̤id"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/text_margin"
|
||||||
|
android:textAppearance="?attr/textAppearanceListItem" />
|
||||||
|
</LinearLayout>
|
13
app/src/main/res/layout/fragment_attendance_list.xml
Normal file
13
app/src/main/res/layout/fragment_attendance_list.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/list"
|
||||||
|
android:name="com.example.palto.ui.attendanceList.AttendanceFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
app:layoutManager="LinearLayoutManager"
|
||||||
|
tools:context=".ui.attendanceList.AttendanceListFragment"
|
||||||
|
tools:listitem="@layout/fragment_attendance_item" />
|
109
app/src/main/res/layout/fragment_login.xml
Normal file
109
app/src/main/res/layout/fragment_login.xml
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingLeft="@dimen/fragment_horizontal_margin"
|
||||||
|
android:paddingTop="@dimen/fragment_vertical_margin"
|
||||||
|
android:paddingRight="@dimen/fragment_horizontal_margin"
|
||||||
|
android:paddingBottom="@dimen/fragment_vertical_margin"
|
||||||
|
tools:context=".ui.login.LoginFragment">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/hostname"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="64dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:autofillHints="@string/prompt_hostname"
|
||||||
|
android:hint="@string/prompt_hostname"
|
||||||
|
android:inputType="text"
|
||||||
|
android:selectAllOnFocus="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/username"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/username"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:autofillHints="@string/prompt_username"
|
||||||
|
android:hint="@string/prompt_username"
|
||||||
|
android:inputType="text"
|
||||||
|
android:selectAllOnFocus="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/password"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/hostname" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/password"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:autofillHints="@string/prompt_password"
|
||||||
|
android:hint="@string/prompt_password"
|
||||||
|
android:imeActionLabel="@string/action_sign_in_short"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:selectAllOnFocus="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/help_message"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/username" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/help_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/help_message"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/login"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/password" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/login"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:layout_marginStart="48dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="48dp"
|
||||||
|
android:layout_marginBottom="64dp"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="@string/action_sign_in"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/help_message"
|
||||||
|
app:layout_constraintVertical_bias="0.2" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="64dp"
|
||||||
|
android:layout_marginEnd="32dp"
|
||||||
|
android:layout_marginBottom="64dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/password"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/password"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0.3" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
20
app/src/main/res/layout/fragment_session_item.xml
Normal file
20
app/src/main/res/layout/fragment_session_item.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/item_number"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/text_margin"
|
||||||
|
android:textAppearance="?attr/textAppearanceListItem" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/text_margin"
|
||||||
|
android:textAppearance="?attr/textAppearanceListItem" />
|
||||||
|
</LinearLayout>
|
13
app/src/main/res/layout/fragment_session_list.xml
Normal file
13
app/src/main/res/layout/fragment_session_list.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/list"
|
||||||
|
android:name="com.example.palto.ui.session.SessionListFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
app:layoutManager="LinearLayoutManager"
|
||||||
|
tools:context=".ui.sessionList.SessionListFragment"
|
||||||
|
tools:listitem="@layout/fragment_session_item" />
|
22
app/src/main/res/navigation/nav_graph.xml
Normal file
22
app/src/main/res/navigation/nav_graph.xml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/nav_graph"
|
||||||
|
app:startDestination="@id/attendanceListFragment">
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/loginFragment"
|
||||||
|
android:name="com.example.palto.ui.login.LoginFragment"
|
||||||
|
android:label="fragment._login"
|
||||||
|
tools:layout="@layout/fragment_login" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/sessionListFragment"
|
||||||
|
android:name="com.example.palto.ui.sessionList.SessionListFragment"
|
||||||
|
android:label="fragment_session_list"
|
||||||
|
tools:layout="@layout/fragment_session_list" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/attendanceListFragment"
|
||||||
|
android:name="com.example.palto.ui.attendanceList.AttendanceListFragment"
|
||||||
|
android:label="fragment_attendance_list"
|
||||||
|
tools:layout="@layout/fragment_attendance_list" />
|
||||||
|
</navigation>
|
6
app/src/main/res/values/dimens.xml
Normal file
6
app/src/main/res/values/dimens.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="fragment_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="fragment_vertical_margin">16dp</dimen>
|
||||||
|
<dimen name="text_margin">16dp</dimen>
|
||||||
|
</resources>
|
|
@ -1,3 +1,15 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Palto</string>
|
<string name="app_name">Palto</string>
|
||||||
|
<!-- Strings related to login -->
|
||||||
|
<string name="prompt_hostname">Serveur</string>
|
||||||
|
<string name="prompt_username">Nom d’utilisateur</string>
|
||||||
|
<string name="prompt_password">Mot de passe</string>
|
||||||
|
<string name="action_sign_in">Connexion</string>
|
||||||
|
<string name="action_sign_in_short">Sign in</string>
|
||||||
|
<string name="welcome">"Bienvenue !"</string>
|
||||||
|
<string name="invalid_hostname">Serveur inaccessible</string>
|
||||||
|
<string name="invalid_username">Nom d’utilisateur non valide</string>
|
||||||
|
<string name="invalid_password">Mot de passe invalide</string>
|
||||||
|
<string name="login_failed">"Erreur de connexion !"</string>
|
||||||
|
<string name="help_message">Identifiants Invalides</string>
|
||||||
</resources>
|
</resources>
|
|
@ -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.1.2" apply false
|
id("com.android.application") version "8.2.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
|
||||||
}
|
}
|
Loading…
Reference in a new issue